Skip to main content

Manager

Managers are singletons that orchestrate the complex asynchronous behavior of Reactive Data Client. Several managers are provided by Reactive Data Client and used by default; however there is nothing stopping other compatible managers to be built that expand the functionality. We encourage PRs or complimentary libraries!

While managers often have complex internal state and methods - the exposed interface is quite simple. Because of this, it is encouraged to keep any supporting state or methods marked at protected by typescript. Managers have three exposed pieces - the constructor to build initial state and take any parameters; a simple cleanup() method to tear down any dangling pieces like setIntervals() or unresolved Promises; and finally getMiddleware() - providing the mechanism to hook into the flux data flow.

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

type Middleware = <R extends React.Reducer<State<unknown>, ActionTypes>>(
controller: Controller,
) => (next: Dispatch<R>) => Dispatch<R>;

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

getMiddleware()

getMiddleware() returns a function that 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 <CacheProvider />. 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.

cleanup()

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

init()

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

Provided managers

Adding managers to Reactive Data Client

Use the managers prop of CacheProvider. Be sure to hoist to module level or wrap in a useMemo() to ensure they are not recreated. Managers have internal state, so it is important to not constantly recreate them.

/index.tsx
import { CacheProvider, getDefaultManagers } from '@data-client/react';
import ReactDOM from 'react-dom';

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

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

Control flow

Managers live in the CacheProvider centralized store. They orchestrate complex control flows by interfacing via intercepting and dispatching actions, as well as reading the internal state.

Manager flux flowManager flux flow

Middleware logging

import type { Manager, Middleware } from '@data-client/core';

export default class LoggingManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
console.log('before', action, controller.getState());
await next(action);
console.log('after', action, controller.getState());
};

cleanup() {}
}

Middleware data stream (push-based)

Adding a manager to process data pushed from the server by websockets or Server Sent Events ensures we can maintain fresh data when the data updates are independent of user action. For example, a trading app's price, or a real-time collaborative editor.

import type { Manager, Middleware } from '@data-client/core';
import type { EndpointInterface } from '@data-client/endpoint';

export default class StreamManager implements Manager {
protected declare middleware: Middleware;
protected declare evtSource: WebSocket | EventSource;
protected declare endpoints: Record<string, EndpointInterface>;

constructor(
evtSource: WebSocket | EventSource,
endpoints: Record<string, EndpointInterface>,
) {
this.evtSource = evtSource;
this.endpoints = endpoints;

this.middleware = controller => {
this.evtSource.onmessage = event => {
try {
const msg = JSON.parse(event.data);
if (msg.type in this.endpoints)
controller.setResponse(this.endpoints[msg.type], ...msg.args, msg.data);
} catch (e) {
console.error('Failed to handle message');
console.error(e);
}
};
return next => async action => next(action);
};
}

cleanup() {
this.evtSource.close();
}

getMiddleware() {
return this.middleware;
}
}

Controller.setResponse() updates the Reactive Data Client store with event.data.