Skip to main content

Manager

Managers are singletons that handle global side-effects. Kind of like useEffect() for the central data store.

The default managers orchestrate the complex asynchronous behavior that Data Client provides out of the box. These can easily be configured with getDefaultManagers(), and extended with your own custom Managers.

Managers must implement middleware, which hooks them into the central store's control flow. Additionally, cleanup() and init() hook into the store's lifecycle for setup/teardown behaviors.

type Dispatch = (action: ActionTypes) => Promise<void>;

type Middleware = (controller: Controller) => (next: Dispatch) => Dispatch;

interface Manager {
middleware: Middleware;
cleanup(): void;
init?: (state: State<any>) => void;
}

Lifecycle

middleware

middleware is very similar to a redux middleware. The only differences is that the next() function returns a Promise. This promise resolves when the reducer update is committed when using <DataProvider />. This is necessary since the commit phase is asynchronously scheduled. This enables building managers that perform work after the DOM is updated and also with the newly computed state.

Since redux is fully synchronous, an adapter must be placed in front of Reactive Data Client style middleware to ensure they can consume a promise. Conversely, redux middleware must be changed to pass through promises.

Middlewares will intercept actions that are dispatched and then potentially dispatch their own actions as well. To read more about middlewares, see the redux documentation.

init(state)

Called with initial state after provider is mounted. Can be useful to run setup at start that relies on state actually existing.

cleanup()

Provides any cleanup of dangling resources after manager is no longer in use.

Adding managers to Reactive Data Client

Use the managers prop of DataProvider with a function that creates them. The provider calls it once when it mounts, so each store gets its own instances and the same definition works for the Next.js provider.

Migrating from arrays

managers also still accepts an array of instances. This form is transitional and will be removed in a future release; change managers={[...]} to managers={() => [...]} when you can. Managers have internal state, so an array must be hoisted to module level or wrapped in useMemo() to avoid recreating them.

/index.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>,
);

Control flow

Managers integrate with the DataProvider store with their lifecycles and middleware. They orchestrate complex control flows by interfacing via intercepting and dispatching actions, as well as reading the internal state.

Manager flux flowManager flux flow

The job of middleware is to dispatch actions, respond to actions, or both.

Dispatching Actions

Controller provides type-safe action dispatchers.

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

actionTypes includes all constants to distinguish between different actions.

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

In conditional blocks, the action type narrows, encouraging safe access to its members.

In case we want to 'handle' a certain action, we can 'consume' it by not calling next.

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 if we use it
          return Promise.resolve();
        }
      default:
        return next(action);
    }
  };

  cleanup() {}

  subscribe(channel: string, product_id: string) {}
  unsubscribe(channel: string, product_id: string) {}
}

By return Promise.resolve(); instead of calling next(action), we prevent managers listed after this one from seeing that action.

Types: FETCH, SET, SET_RESPONSE, RESET, SUBSCRIBE, UNSUBSCRIBE, INVALIDATE, INVALIDATEALL, EXPIREALL

Use cases

Minimal examples for common Manager use cases: