| name | data-client-manager |
| description | Implement @data-client Managers for global/background side effects - websocket, SSE, polling, real-time updates, subscriptions, logging, analytics, middleware, intercepting Controller actions, DataProvider managers prop, redux-style action handling. Use when adding cross-cutting store behavior, reacting to dispatched actions, or handling external event streams. |
| license | Apache 2.0 |
Guide: Using @data-client Managers for global side effects
Managers are singletons that handle global side-effects. Kind of like useEffect() for the central data store.
They interface with the store using Controller, and redux middleware is run in response to actions.
Single Responsibility
One concern per Manager; compose many small managers (e.g. transport, subscriptions, logging, auth) rather than one large one.
References
For detailed API documentation, see the references directory:
Always use actionTypes when comparing action.type. Refer to Actions for list of actions and their payloads.
Dispatching actions
Controller has dispatchers:
ctrl.fetch(), ctrl.fetchIfStale(), ctrl.expireAll(), ctrl.invalidate(), ctrl.invalidateAll(), ctrl.setResponse(), ctrl.set(),
ctrl.setError(), ctrl.resetEntireStore(), ctrl.subscribe(), ctrl.unsubscribe().
import type { Manager, Middleware } from '@data-client/core';
import CurrentTime from './CurrentTime';
export default class TimeManager implements Manager {
protected declare 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);
}
}
Reading and Consuming Actions
Controller has data accessors:
ctrl.getResponse(), ctrl.getState(), ctrl.get(), ctrl.getError(), ctrl.snapshot().
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)}`,
);
await next(action);
const { data } = controller.getResponse(
action.endpoint,
...action.args,
controller.getState(),
);
console.info(`${action.endpoint.name} ${JSON.stringify(data)}`);
return;
}
default:
return next(action);
}
};
cleanup() {}
}
Always use actionTypes members to check action.type.
actionTypes has: FETCH, SET, SET_RESPONSE, RESET, SUBSCRIBE, UNSUBSCRIBE, INVALIDATE, INVALIDATEALL, EXPIREALL
actions docs details the action types and their payloads.
Consuming actions
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 {
protected declare entities: Record<string, EntityInterface>;
middleware: Middleware = controller => next => async action => {
switch (action.type) {
case actionTypes.SUBSCRIBE:
case actionTypes.UNSUBSCRIBE:
const { schema } = action.endpoint;
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);
}
return Promise.resolve();
}
default:
return next(action);
}
};
cleanup() {}
subscribe(channel: string, product_id: string) {}
unsubscribe(channel: string, product_id: string) {}
}
Usage
import { DataProvider, getDefaultManagers } from '@data-client/react';
import ReactDOM from 'react-dom';
const managers = [...getDefaultManagers(), new MyManager()];
ReactDOM.createRoot(document.body).render(
<DataProvider managers={managers}>
<App />
</DataProvider>,
);