home / skills / reactive / data-client / data-client-manager

data-client-manager skill

/.cursor/skills/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-manager

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.9 KB
---
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>,
);
```

Overview

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.

How this skill works

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.

When to use it

  • You need a single place to manage long-lived side effects (websockets, SSE, polling).
  • You want centralized logging or analytics triggered by store actions.
  • You need to translate SUBSCRIBE/UNSUBSCRIBE actions into external subscriptions or channels.
  • You must perform periodic updates or background refresh using controller.set or controller.fetch.
  • You want middleware-like behavior that can read state and decide whether to consume an action.

Best practices

  • Always compare action.type using the provided actionTypes constants to avoid string mismatches.
  • Perform external side-effect setup in middleware(controller) so you can access controller methods and state.
  • Call next(action) for actions you do not fully consume; return a resolved Promise if you fully handle an action.
  • Use controller.getResponse/getState/getError/snapshot to read the latest store before making decisions.
  • Implement cleanup to clear timers, close sockets, and release resources when the manager is torn down.

Example use cases

  • A TimeManager that sets a central CurrentTime resource every second using controller.set.
  • A LoggingManager that logs SET_RESPONSE payloads and then reads the finalized data from the store.
  • A WebsocketManager that opens a socket on SUBSCRIBE, routes incoming messages to controller.setResponse, and closes on UNSUBSCRIBE.
  • A PollingManager that dispatches controller.fetchIfStale on an interval and invalidates stale cache entries.
  • A CustomSubsManager that maps application-level subscriptions to external channels and prevents other managers from handling those actions.

FAQ

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.