CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

data-client-manager

Write @data-client Managers for app-wide side effects and real-time data such as websockets, SSE and polling: code that reacts to every request or to events from outside the app and updates the store. Use one instead of useEffect for behavior that belongs to the whole app, such as error toasts and reporting, logging and analytics, auth failures, refetching on focus or reconnect, and cross-tab sync.

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Guide: Using @data-client Managers for global side effects

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

Every Manager defines a middleware property and a cleanup() method. A Manager with nothing to intercept passes actions through: middleware: Middleware = controller => next => async action => next(action);

Each store gets its own Managers, so code lives at one of three lifetimes:

LifetimeLastsHolds
Configthe pageconstructor options, like a URL or callbacks. The constructor only stores them
Storeone storeinstance fields about the store, like the controller and fetches in flight
Activeinit() to cleanup()timers, intervals, connections and listeners

cleanup() pauses Managers without ending the store: React StrictMode and <Activity> hiding DataProvider call cleanup() and later init() on the same store, and actions keep arriving in between. So:

  • Keep the controller from middleware (this.controller = controller) to use in init(). init(state) gets the store's current state, also on resume.
  • Don't reset state about the store, like fetches in flight, in cleanup().
  • Guard side effects in middleware with a field cleanup() clears, like this.channel?.postMessage().

Use cases

Minimal working examples for each use case live in references/managers.md (see the named section) unless another file is linked:

Use caseReferenceKey technique
Logging actions/state"Middleware logging" in managers.mdlog around await next(action)
Error reporting/monitoring (Sentry)"Error reporting" in managers.mdSET_RESPONSE with action.error
Analytics/metrics (fetch timing)"Metrics" in managers.mdFETCH action's action.meta.promise
Toast notifications on mutations"Notifications" in managers.mdSET_RESPONSE with action.endpoint.sideEffect
Refetch on window focus / network reconnect"Refresh on focus or reconnect" in managers.mdcontroller.expireAll() from event listeners in init()/cleanup()
Cross-tab sync"Cross-tab synchronization" in managers.mdBroadcastChannel + controller.expireAll()
Offline persistence (IndexedDB)"Offline persistence" in managers.mddebounced 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.mdcontroller.set() on message; connect in init(), close in cleanup()
High-frequency streams / snapshots"Batching high-frequency updates" in managers.mdbuffer, then one controller.set([Entity], rows) per flush
Polling/interval updates (ticker)"Dispatching Actions" in Manager.md; TimeManager belowsetInterval in init() + controller.set()
Custom transport subscriptions"Reading and Consuming Actions" in Manager.mdconsume SUBSCRIBE/UNSUBSCRIBE without calling next
Auth: logout on 401, reset store on deauthLogoutManager.mdhandleLogout(controller) + controller.resetEntireStore()

References

Vue projects: read <name>.vue.md instead of <name>.md when it exists.

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 { Controller, Manager, Middleware } from '@data-client/react'; // or '@data-client/vue'
import CurrentTime from './CurrentTime';

export default class TimeManager implements Manager {
  declare protected controller: Controller;
  declare protected intervalID?: ReturnType<typeof setInterval>;

  middleware: Middleware = controller => {
    this.controller = controller;
    return next => async action => next(action);
  };

  init() {
    this.intervalID = setInterval(() => {
      this.controller.set(CurrentTime, { id: 1 }, { id: 1, time: Date.now() });
    }, 1000);
  }

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

Pass a function that returns the managers, defined at module level; it runs once per store. Create each Manager inside the function: an instance made outside it is shared by every store.

List custom managers before ...getDefaultManagers(). Actions pass through managers in array order, and NetworkManager (one of the defaults) resolves each FETCH itself instead of passing it on (only optimistic mutations continue), so a manager listed after it misses most fetches:

import { DataProvider, getDefaultManagers } from '@data-client/react';
import { createRoot } from 'react-dom/client';

const managers = () => [new MyManager(), ...getDefaultManagers()];

createRoot(document.body).render(
  <DataProvider managers={managers}>
    <App />
  </DataProvider>,
);
Repository

reactive/data-client

Path

.agents/skills/data-client-manager/SKILL.md

Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.