home / skills / reactive / data-client / data-client-manager
This skill helps you manage global side effects with @data-client Managers, coordinating websockets, SSE, polling, and middleware for a reactive app.
npx playbooks add skill reactive/data-client --skill data-client-managerReview the files below or copy the command above to add this skill to your agents.
---
name: data-client-manager
description: Implement @data-client Managers for global side effects - websocket, SSE, polling, subscriptions, logging, middleware, Controller actions, redux pattern
license: Apache 2.0
---
# Guide: Using `@data-client` Managers for global side effects
[Managers](references/managers.md) are singletons that handle global side-effects. Kind of like useEffect() for the central data store.
They interface with the store using [Controller](references/Controller.md), and [redux middleware](https://redux.js.org/tutorials/fundamentals/part-4-store#middleware) is run in response to [actions](references/Actions.md).
## References
For detailed API documentation, see the [references](references/) directory:
- [Manager](references/Manager.md) - Manager interface and lifecycle
- [Actions](references/Actions.md) - Action types and payloads
- [Controller](references/Controller.md) - Imperative actions
- [LogoutManager](references/LogoutManager.md) - Handling logout/cleanup
- [getDefaultManagers](references/getDefaultManagers.md) - Default manager configuration
- [managers](references/managers.md) - Managers concept guide
Always use `actionTypes` when comparing action.type. Refer to [Actions](references/Actions.md) for list of actions and their payloads.
## Dispatching actions
[Controller](references/Controller.md) has dispatchers:
ctrl.fetch(), ctrl.fetchIfStale(), ctrl.expireAll(), ctrl.invalidate(), ctrl.invalidateAll(), ctrl.setResponse(), ctrl.set(),
ctrl.setError(), ctrl.resetEntireStore(), ctrl.subscribe(), ctrl.unsubscribe().
```ts
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](references/Controller.md) has data accessors:
ctrl.getResponse(), ctrl.getState(), ctrl.get(), ctrl.getError(), ctrl.snapshot().
```ts
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 to React
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() {}
}
```
Always use `actionTypes` members to check action.type.
`actionTypes` has: FETCH, SET, SET_RESPONSE, RESET, SUBSCRIBE, UNSUBSCRIBE, INVALIDATE, INVALIDATEALL, EXPIREALL
[actions](references/Actions.md) docs details the action types and their payloads.
## Consuming actions
```ts
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;
// 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 to prevent it from being processed by other managers
return Promise.resolve();
}
default:
return next(action);
}
};
cleanup() {}
subscribe(channel: string, product_id: string) {}
unsubscribe(channel: string, product_id: string) {}
}
```
## Usage
```tsx
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>,
);
```
This skill implements @data-client Managers to handle global side effects for async state management in TypeScript. It provides a pattern for centralizing websockets, Server-Sent Events, polling, subscriptions, logging, middleware, and controller-driven actions across your app. Managers live as singletons and integrate with the controller and redux-style middleware to run and react to actions.
Managers expose a middleware function that receives a Controller and returns a redux-style middleware to intercept actions. Use the Controller API (dispatchers and accessors) to trigger fetches, set responses, subscribe/unsubscribe, read store state, and perform imperatively controlled updates. Managers also provide a cleanup lifecycle to stop intervals, close sockets, or tear down other side effects.
How do I read the current data before dispatching a side effect?
Use controller.getResponse, controller.getState, or controller.snapshot to read the latest store data inside your middleware before dispatching actions.
When should a manager consume an action instead of passing it on?
Consume actions when the manager fully handles the side effect (e.g., opening a socket for SUBSCRIBE) and no further processing is needed; otherwise call next(action) so other managers and reducers can process it.