Skip to main content

Controller

class Controller {
/*************** Action Dispatchers ***************/
fetch(endpoint, ...args) => ReturnType<E>;
invalidate(endpoint, ...args) => Promise<void>;
resetEntireStore: () => Promise<void>;
receive(endpoint, ...args, response) => Promise<void>;
receiveError(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;
}

useController() provides access in React components

fetch(endpoint, ...args)

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

function CreatePost() {
const { fetch } = useController();

return (
<form onSubmit={e => 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 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 { invalidate } = useController();

return (
<div>
<h1>{article.title}<h1>
<button onClick={() => 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.

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 { resetEntireStore } = useController();

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

receive(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 { receive } = useController();

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

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

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

receiveError(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 receive, 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 and not during React's render lifecycle.

const controller = useController();

const updateHandler = useCallback(
async updatePayload => {
const response = await controller.fetch(
MyResource.update,
{ id },
updatePayload,
);
const denormalized = controller.getResponse(
MyResource.update,
{ id },
updatePayload,
controller.getState(),
);
redirect(denormalized.getterUrl);
},
[id],
);