Saltar al contenido principal

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.

index.tsx
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>,
);

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.

Flujo flux del ManagerFlujo flux del Manager

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: