data-client-manager
reactive/data-client/.agents/skills/data-client-manager/SKILL.md
Implement @data-client Managers for global/background side effects - websocket, SSE, polling, real-time updates, subscriptions, logging, analytics, metrics/timing, error reporting (Sentry), toast notifications, refetch on window focus or network reconnect, cross-tab sync (BroadcastChannel), offline persistence (localStorage/IndexedDB), auth logout on 401, 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.
What's in it
- Guide: Using @data-client Managers for global side effects
- Single Responsibility
- Use cases
- References
- Dispatching actions
- Reading and Consuming Actions
- Consuming actions
- Usage
---
name: data-client-manager
description: Implement @data-client Managers for global/background side effects - websocket, SSE, polling, real-time updates, subscriptions, logging, analytics, metrics/timing, error reporting (Sentry), toast notifications, refetch on window focus or network reconnect, cross-tab sync (BroadcastChannel), offline persistence (localStorage/IndexedDB), auth logout on 401, 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](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).
## Single Responsibility
One concern per Manager; compose many small managers (e.g. transport, subscriptions, logging, auth) rather than one large one.
## Use cases
Minimal working examples for each use case live in [references/managers.md](references/managers.md) (see the named section) unless another file is linked:
| Use case | Reference | Key technique |
| ------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Logging actions/state | "Middleware logging" in [managers.md](references/managers.md) | log around `await next(action)` |
| Error reporting/monitoring (Sentry) | "Error reporting" in [managers.md](references/managers.md) | `SET_RESPONSE` with `action.error` |
| Analytics/metrics (fetch timing) | "Metrics" in [managers.md](references/managers.md) | `FETCH` action's `action.meta.promise` |
| Toast notifications on mutations | "Notifications" in [managers.md](references/managers.md) | `SET_RESPONSE` with `action.endpoint.sideEffect` |
| Refetch on window focus / network reconnect | "Refresh on focus or reconnect" in [managers.md](references/managers.md) | `controller.expireAll()` from event listeners in `init()`/`cleanup()` |
| Cross-tab sync | "Cross-tab synchronization" in [managers.md](references/managers.md) | BroadcastChannel + `controller.expireAll()` |
| Offline persistence (IndexedDB) | "Offline persistence" in [managers.md](references/managers.md) | debounced IndexedDB write of `controller.getState()` (drop `optimistic` - not cloneable); restore via DataProvider `initialState`. Never use localStorage (blocking) |
| Websocket/SSE push streams | "Middleware data stream" in [managers.md](references/managers.md) | `controller.set()` on message; connect in `init()`, close in `cleanup()` |
| High-frequency streams / snapshots | "Batching high-frequency updates" in [managers.md](references/managers.md) | buffer, then one `controller.set([Entity], rows)` per flush |
| Polling/interval updates (ticker) | "Dispatching Actions" in [Manager.md](references/Manager.md); `TimeManager` below | `setInterval` + `controller.set()` |
| Custom transport subscriptions | "Reading and Consuming Actions" in [Manager.md](references/Manager.md) | consume `SUBSCRIBE`/`UNSUBSCRIBE` without calling `next` |
| Auth: logout on 401, reset store on deauth | [LogoutManager.md](references/LogoutManager.md) | `handleLogout(controller)` + `controller.resetEntireStore()` |
## References
Vue projects: read `<name>.vue.md` instead of `<name>.md` when it exists.
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/react'; // or '@data-client/vue'
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);
}
}
```
Write many entities in one store update with `controller.set([Entity], rows)` (websocket snapshots, buffered stream messages). Never loop `controller.set(Entity, args, row)` per row, and never add an endpoint or `setResponse()` just to batch.
## 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 {
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 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 { createRoot } from 'react-dom/client';
const managers = [...getDefaultManagers(), new MyManager()];
createRoot(document.body).render(
<DataProvider managers={managers}>
<App />
</DataProvider>,
);
```
More agent context in reactive/data-client
28 other files this repository gives its agents.
Cursor rule
Skill
- changeset.agents/skills/changeset/SKILL.md
- data-client-endpoint-setup.agents/skills/data-client-endpoint-setup/SKILL.md
- data-client-graphql-setup.agents/skills/data-client-graphql-setup/SKILL.md
- data-client-react.agents/skills/data-client-react/SKILL.md
- data-client-react-testing.agents/skills/data-client-react-testing/SKILL.md
- data-client-rest-setup.agents/skills/data-client-rest-setup/SKILL.md
- data-client-rest.agents/skills/data-client-rest/SKILL.md
- data-client-schema.agents/skills/data-client-schema/SKILL.md
- data-client-setup.agents/skills/data-client-setup/SKILL.md
- data-client-v0.18-migration.agents/skills/data-client-v0.18-migration/SKILL.md
- data-client-vue.agents/skills/data-client-vue/SKILL.md
- data-client-vue-testing.agents/skills/data-client-vue-testing/SKILL.md
- frontend-design.agents/skills/frontend-design/SKILL.md
- initialize.agents/skills/initialize/SKILL.md
- interface-design.agents/skills/interface-design/SKILL.md
- packages-documentation.agents/skills/packages-documentation/SKILL.md
- path-to-regexp-v8-migration.agents/skills/path-to-regexp-v8-migration/SKILL.md
- pr.agents/skills/pr/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

