reactive/data-client

data-client-manager

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 beh…

First seen Feb 16, 2026

Installation

$ npx skills add reactive/data-client --skill data-client-manager

Summary

  • 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.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from reactive/data-client · top by installs.

npx skills add reactive/data-client

Browse all from reactive/data-client

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2.0K
License LICENSE
Default branch master
Open issues 3
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

LicenseApache 2.0

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 9,686 B
  • docs SUMMARY.md 600 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 33 installs

SKILL.md

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 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()
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

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().

import type { Manager, Middleware } from '@data-client/core';
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);
  }
}

Reading and Consuming Actions

[Controller](references/Controller.md) 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)}`,
          );
          // 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

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

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>,
);