Skip to main content

Controller

class Controller {
/*************** Action Dispatchers ***************/
fetch(endpoint, ...args): ReturnType<E>;
invalidate(endpoint, ...args): Promise<void>;
invalidateAll({ testKey }): Promise<void>;
resetEntireStore(): Promise<void>;
setResponse(endpoint, ...args, response): Promise<void>;
setError(endpoint, ...args, error): Promise<void>;
resolve(endpoint, { args, response, fetchedAt, error }): Promise<void>;
subscribe(endpoint, ...args): Promise<void>;
unsubscribe(endpoint, ...args): Promise<void>;
/*************** Data Access ***************/
getResponse(endpoint, ...args, state): { data, expiryStatus, expiresAt };
getError(endpoint, ...args, state): ErrorTypes | undefined;
snapshot(state: State<unknown>, fetchedAt?: number): SnapshotInterface;
getState(): State<unknown>;
}

Controller is a singleton providing safe access to the Rest Hooks flux store and lifecycle.

useController() provides access in React components, and for Managers it is passed as the first argument in Manager.getMiddleware()

fetch(endpoint, ...args)

Fetches the endpoint with given args, updating the Rest Hooks cache with the response or error upon completion.

function CreatePost() {
const ctrl = useController();

return (
<form
onSubmit={e => ctrl.fetch(PostResource.create, new FormData(e.target))}
>
{/* ... */}
</form>
);
}
tip

fetch has the same return value as the Endpoint passed to it. When using schemas, the denormalized value can be retrieved using a combination of Controller.getResponse and Controller.getState

await controller.fetch(PostResource.create, createPayload);
const { data: denormalizedResponse } = controller.getResponse(
PostResource.create,
createPayload,
controller.getState(),
);

Endpoint.sideEffect

sideEffect changes the behavior

true

  • Resolves before committing Rest Hooks cache updates.
  • Each call will always cause a new fetch.

undefined

  • Resolves after committing Rest Hooks cache updates.
  • Identical requests are deduplicated globally; allowing only one inflight request at a time.
    • To ensure a new request is started, make sure to abort any existing inflight requests.

invalidate(endpoint, ...args)

Forces refetching and suspense on useSuspense with the same Endpoint and parameters.

function ArticleName({ id }: { id: string }) {
const article = useSuspense(ArticleResource.get, { id });
const ctrl = useController();

return (
<div>
<h1>{article.title}<h1>
<button onClick={() => ctrl.invalidate(ArticleResource.get, { id })}>Fetch &amp; suspend</button>
</div>
);
}
tip

To refresh while continuing to display stale data - Controller.fetch instead.

Invalidate many endpoints at once

Use schema.Delete to invalidate every endpoint that contains a given entity.

invalidateAll({ testKey })

Invalidates all endpoint keys matching testKey.

function ArticleName({ id }: { id: string }) {
const article = useSuspense(ArticleResource.get, { id });
const ctrl = useController();

return (
<div>
<h1>{article.title}<h1>
<button onClick={() => ctrl.invalidateAll(ArticleResource.get)}>Fetch &amp; suspend</button>
</div>
);
}

Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache.

const myDomain = 'http://test.com';

function useLogout() {
const ctrl = useController();
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);
return () => ctrl.invalidateAll({ testKey });
}

resetEntireStore()

Resets/clears the entire Rest Hooks cache. All inflight requests will not resolve.

This is typically used when logging out or changing authenticated users.

const USER_NUMBER_ONE: string = "1111";

function UserName() {
const user = useSuspense(CurrentUserResource.get);
const ctrl = useController();

const becomeAdmin = useCallback(() => {
// Changes the current user
impersonateUser(USER_NUMBER_ONE);
ctrl.resetEntireStore();
}, [ctrl]);
return (
<div>
<h1>{user.name}<h1>
<button onClick={becomeAdmin}>Be Number One</button>
</div>
);
}

receive()

Another name for setResponse()

setResponse(endpoint, ...args, response)

Stores response in cache for given Endpoint and args.

Any components suspending for the given Endpoint and args will resolve.

If data already exists for the given Endpoint and args, it will be updated.

const ctrl = useController();

useEffect(() => {
const websocket = new Websocket(url);

websocket.onmessage = event =>
ctrl.setResponse(EndpointLookup[event.endpoint], ...event.args, event.data);

return () => websocket.close();
});

This shows a proof of concept in React; however a Manager websockets implementation would be much more robust.

receiveError()

Another name for setError()

setError(endpoint, ...args, error)

Stores the result of Endpoint and args as the error provided.

resolve(endpoint, { args, response, fetchedAt, error })

Resolves a specific fetch, storing the response in cache.

This is similar to setResponse, except it triggers resolution of an inflight fetch. This means the corresponding optimistic update will no longer be applies.

This is used in NetworkManager, and should be used when processing fetch requests.

subscribe(endpoint, ...args)

Marks a new subscription to a given Endpoint. This should increment the subscription.

useSubscription calls this on mount.

This might be useful for custom hooks to sub/unsub based on other factors.

const controller = useController();
const key = endpoint.key(...args);

useEffect(() => {
controller.subscribe(endpoint, ...args);
return () => controller.unsubscribe(endpoint, ...args);
}, [controller, key]);

unsubscribe(endpoint, ...args)

Marks completion of subscription to a given Endpoint. This should decrement the subscription and if the count reaches 0, more updates won't be received automatically.

useSubscription calls this on unmount.

getResponse(endpoint, ...args, state)

returns
{
data: DenormalizeNullable<E['schema']>;
expiryStatus: ExpiryStatus;
expiresAt: number;
}

Gets the (globally referentially stable) response for a given endpoint/args pair from state given.

data

The denormalize response data. Guarantees global referential stability for all members.

expiryStatus

export enum ExpiryStatus {
Invalid = 1,
InvalidIfStale,
Valid,
}

Valid

  • Will never suspend.
  • Might fetch if data is stale

InvalidIfStale

  • Will suspend if data is stale.
  • Might fetch if data is stale

Invalid

  • Will always suspend
  • Will always fetch

expiresAt

A number representing time when it expires. Compare to Date.now().

Example

This is used in useCache, useSuspense and can be used in Managers to lookup a response with the state provided.

useCache.ts
import {
useController,
StateContext,
EndpointInterface,
} from '@rest-hooks/core';

/** Oversimplified useCache */
function useCache<E extends EntityInterface>(
endpoint: E,
...args: readonly [...Parameters<E>]
) {
const state = useContext(StateContext);
const controller = useController();
return controller.getResponse(endpoint, ...args, state).data;
}
MyManager.ts
import type { Manager, Middleware, actionTypes } from '@rest-hooks/core';
import type { EndpointInterface } from '@rest-hooks/endpoint';

export default class MyManager implements Manager {
protected declare middleware: Middleware;
constructor() {
this.middleware = ({ controller, getState }) => {
return next => async action => {
if (action.type === actionTypes.FETCH_TYPE) {
console.log('The existing response of the requested fetch');
console.log(
controller.getResponse(
action.endpoint,
...(action.meta.args as Parameters<typeof action.endpoint>),
getState(),
).data,
);
}
next(action);
};
};
}

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

getMiddleware<T extends StreamManager>(this: T) {
return this.middleware;
}
}

getError(endpoint, ...args, state)

Gets the error, if any, for a given endpoint. Returns undefined for no errors.

snapshot(state, fetchedAt)

Returns a Snapshot.

getState()

Gets the internal state of Rest Hooks that has already been committed.

danger

This should only be used in event handlers or Managers.

Using getState() in React's render lifecycle can result in data tearing.

const controller = useController();

const updateHandler = useCallback(
async updatePayload => {
const response = await controller.fetch(
MyResource.update,
{ id },
updatePayload,
);
// the fetch has completed, but react has not yet re-rendered
// this lets use sequence after the next re-render
// we're working on a better solution to this specific case
setTimeout(() => {
const { data: denormalized } = controller.getResponse(
MyResource.update,
{ id },
updatePayload,
controller.getState(),
);
redirect(denormalized.getterUrl);
}, 40);
},
[id],
);