Manager
Los Managers son singletons que manejan efectos secundarios globales. Algo parecido a useEffect() para el store de datos
central.
Los managers predeterminados orquestan el complejo comportamiento asíncrono que Data Client
ofrece de serie. Se pueden configurar fácilmente con getDefaultManagers() y
ampliar con tus propios Managers personalizados.
Los managers deben implementar middleware, que los engancha al flujo de control del store central. Además, cleanup() e init() se enganchan al ciclo de vida del store para los comportamientos de preparación y desmontaje.
type Dispatch = (action: ActionTypes) => Promise<void>;
type Middleware = (controller: Controller) => (next: Dispatch) => Dispatch;
interface Manager {
middleware: Middleware;
cleanup(): void;
init?: (state: State<any>) => void;
}
Ciclo de vida
middleware
middleware es muy parecido a un middleware de redux.
La única diferencia es que la función next() devuelve una Promise.
Esta promesa se resuelve cuando la actualización del reducer se confirma (commit) al usar <DataProvider />. Esto es necesario porque la fase de commit se programa de forma asíncrona. Permite construir managers que realizan trabajo una vez actualizado el DOM y con el estado recién calculado.
Como redux es totalmente síncrono, hay que colocar un adaptador delante de los middlewares al estilo de Reactive Data Client para que puedan consumir una promesa. A la inversa, los middlewares de redux deben modificarse para que dejen pasar las promesas.
Los middlewares interceptan las acciones que se despachan y, además, pueden despachar sus propias acciones. Para saber más sobre los middlewares, consulta la documentación de redux.
init(state)
Se llama con el estado inicial después de montar el provider. Puede ser útil para ejecutar una preparación inicial que depende de que el estado exista realmente.
cleanup()
Se encarga de limpiar los recursos pendientes cuando el manager deja de usarse.
Añadir managers a Reactive Data Client
Usa la prop managers de DataProvider. Asegúrate de declararlos a nivel de módulo o de envolverlos en un useMemo() para que no se vuelvan a crear. Los managers tienen estado interno, así que es importante no recrearlos constantemente.
- Web
- React Native
- NextJS
- Expo
import { DataProvider, getDefaultManagers } from '@data-client/react';
import { createRoot } from 'react-dom/client';
import App from './App';
import MyManager from './MyManager';
const managers = [...getDefaultManagers(), new MyManager()];
createRoot(document.body).render(
<DataProvider managers={managers}>
<App />
</DataProvider>,
);
import { DataProvider, getDefaultManagers } from '@data-client/react';
import { AppRegistry } from 'react-native';
import App from './App';
import MyManager from './MyManager';
const managers = [...getDefaultManagers(), new MyManager()];
const Root = () => (
<DataProvider managers={managers}>
<App />
</DataProvider>
);
AppRegistry.registerComponent('MyApp', () => Root);
'use client';
import { getDefaultManagers } from '@data-client/react';
import { DataProvider } from '@data-client/react/nextjs';
import MyManager from './MyManager';
const managers = [...getDefaultManagers(), new MyManager()];
export default function Provider({
children,
}: {
children: React.ReactNode;
}) {
return <DataProvider managers={managers}>{children}</DataProvider>;
}
import Provider from './Provider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Provider>{children}</Provider>
</body>
</html>
);
}
import { Stack } from 'expo-router';
import { DataProvider, getDefaultManagers } from '@data-client/react';
import MyManager from './MyManager';
const managers = [...getDefaultManagers(), new MyManager()];
export default function RootLayout() {
return (
<DataProvider managers={managers}>
<Stack>
<Stack.Screen name="index" />
</Stack>
</DataProvider>
);
}
Flujo de control
Los managers se integran con el store de DataProvider mediante sus ciclos de vida y su middleware. Orquestan flujos de control complejos interceptando y despachando acciones, además de leer el estado interno.

El trabajo del middleware es despachar acciones, responder a las acciones, o ambas cosas.
Despachar acciones
Controller ofrece despachadores de acciones con tipado seguro.
import type { Manager, Middleware } from '@data-client/react'; import CurrentTime from './CurrentTime'; export default class TimeManager implements Manager { declare protected intervalID?: ReturnType<typeof setInterval>; middleware: Middleware = controller => { this.intervalID = setInterval(() => { controller.set(CurrentTime, { id: 1 }, { id: 1, time: Date.now() }); }, 1000); return next => async action => next(action); }; cleanup() { clearInterval(this.intervalID); } }
Leer y consumir acciones
actionTypes incluye todas las constantes para distinguir entre las distintas acciones.
import type { Manager, Middleware } from '@data-client/react'; import { actionTypes } from '@data-client/react'; export default class LoggingManager implements Manager { middleware: Middleware = controller => next => async action => { switch (action.type) { case actionTypes.SET_RESPONSE: if (action.endpoint.sideEffect) { console.info( `${action.endpoint.name} ${JSON.stringify(action.response)}`, ); // wait for state update to be committed await next(action); // get the data from the store, which may be merged with existing state const { data } = controller.getResponse( action.endpoint, ...action.args, controller.getState(), ); console.info(`${action.endpoint.name} ${JSON.stringify(data)}`); return; } // actions must be explicitly passed to next middleware default: return next(action); } }; cleanup() {} }
En los bloques condicionales, el tipo de la acción se acota, lo que favorece un acceso seguro a sus miembros.
Si queremos 'manejar' una acción concreta, podemos 'consumirla' sin llamar a next.
import type { Manager, Middleware, EntityInterface, } from '@data-client/react'; import { actionTypes } from '@data-client/react'; import isEntity from './isEntity'; export default class CustomSubsManager implements Manager { declare protected entities: Record<string, EntityInterface>; middleware: Middleware = controller => next => async action => { switch (action.type) { case actionTypes.SUBSCRIBE: case actionTypes.UNSUBSCRIBE: const { schema } = action.endpoint; // only process registered entities if (schema && isEntity(schema) && schema.key in this.entities) { if (action.type === actionTypes.SUBSCRIBE) { this.subscribe(schema.key, action.args[0]?.product_id); } else { this.unsubscribe(schema.key, action.args[0]?.product_id); } // consume subscription if we use it return Promise.resolve(); } default: return next(action); } }; cleanup() {} subscribe(channel: string, product_id: string) {} unsubscribe(channel: string, product_id: string) {} }
Al hacer return Promise.resolve(); en lugar de llamar a next(action), evitamos que los managers listados
después de este vean esa acción.
Tipos: FETCH, SET, SET_RESPONSE,
RESET, SUBSCRIBE, UNSUBSCRIBE,
INVALIDATE, INVALIDATEALL, EXPIREALL
Casos de uso
Ejemplos mínimos para casos de uso comunes de los Manager:
- Registro de logs
- Reporte de errores (monitorización)
- Métricas (tiempos de fetch)
- Notificaciones (toasts)
- Actualizar al recuperar el foco o al reconectar
- Sincronización entre pestañas
- Persistencia sin conexión
- Flujos de datos (websockets/SSE)
- Autenticación: cerrar sesión ante un 401
- Actualizaciones periódicas (intervalo/ticker)
- Suscripciones de transporte personalizadas