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.
- Create
- Update
- Delete
function CreatePost() {
const { fetch } = useController();
return (
<form onSubmit={e => fetch(PostResource.create, new FormData(e.target))}>
{/* ... */}
</form>
);
}
function UpdatePost({ id }: { id: string }) {
const { fetch } = useController();
return (
<form
onSubmit={e => fetch(PostResource.update, { id }, new FormData(e.target))}
>
{/* ... */}
</form>
);
}
function PostListItem({ post }: { post: PostResource }) {
const { fetch } = useController();
const handleDelete = useCallback(
async e => {
await fetch(PostResource.delete, { id: post.id });
history.push('/');
},
[fetch, id],
);
return (
<div>
<h3>{post.title}</h3>
<button onClick={handleDelete}>X</button>
</div>
);
}
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 & suspend</button>
</div>
);
}
To refresh while continuing to display stale data - Controller.fetch instead.
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)
{
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.
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;
}
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.
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],
);