useApi
Simple fetch hook built on Zustand and Immer. Inspired by
useSwr().STATUS: This component is in preview.
- The prop-interface has pretty much stabilized
- The component has typically been tested out in the field
- Consult the design team for usage
- As always: Your feedback is welcomed!
Install
yarn add @ntds/stickyBasics
The following example showcases the basic and minimal usage of useApi:
export function MyComponent() {
const { data, error, working } = useApi<GamesResponse>({
url: '/api/games/list',
});
if (error) {
return <p>Unable to get games: Got {error}</p>;
} else if (!data && working) {
return <Loading />;
}
return <p>You have {data?.games.length} games</p>;
}In most cases you want to make a custom hook around useApi().
This is especially smart to do if you are calling the same api
multiple cases. Here are the same hook again, but is now implemented
in a custom hook:
export function useGameApi() {
return useApi<GamesResponse>({
url: '/api/games/list',
});
}Making the usage cleaner and easier to read:
export function MyComponent() {
const { data, error, working } = useGameApi();
// ....
}initialData (option)
You may have notice in the above examples that we guarded against null values by addressing the games like data?.games.
You could also define the initialValue option to make the hook fall back on this value instead of null:
function MyComponent() {
const { data } = useApi<LobbyResponse>({
url: '/api/result/list',
initialData: {
games: [],
floors: []
},
});
return <p>There are {data.games.length}</p>
}Now, TypeScript will allow you to use the data without a null-guard.
SWR & Cache/Storage
The hook uses the Stale While Revalidate (SWR) pattern out of the box. This means that the data from previous fetches (for the same URL and fetcher) will be used while the hook revalidates the data in the background.
The "staleness" will be checked:
- each time the hook mounted
- reactively when the URL or fetcher changes reactive
- when the data is retrieved from the persistent cache/storage
staleAfter (option)
default value for
staleAfteris2s
In some cases, for examples when the data changes infrequently, it might be overkill to
revalidate the data every time the hook is used. To handle this, use the staleAfteroption.
It tells useApi() how long it should consider the data pristine / not needing revalidation:
export function useGameApi() {
return useApi<GameResults>({
url: '/api/result/list',
staleAfter: '5m',
});
}expireAfter (option)
default value for
expireAfterisInfinity(never)
There can also be cases where it is better to just discard the
previous fetched data and show a spinner / skeleton loader instead.
To tell useApi to do this, use the expireAfter prop, like so:
export function useGameApi() {
return useApi<GameLiveResults>({
url: '/api/result/live',
expireAfter: '1d',
});
}NOTE: It can be combined with the staleAfter 🙏
storage / storageId (option)
Default, useApi() only persists in the SPA life-cycle (looses date when you reload the page).
useApi() also support to persist its data in a provided storage like sessionStorage or localStorage
(or any storage that follows the same API), like so:
export function useGameApi() {
return useApi<OpeningHours>({
url: '/api/time/opening-hours',
staleAfter: '1d',
expireAfter: '1d',
storage: sessionStorage,
storageId: 'opening-hours'
});
}In this example, the data is persisted under the key opening-hours in the sessionStorage.
The data will not be revalidated within this time (staleAfter: '1d') and will be discarded
after one day (expireAfter: '1d')
NOTE: Use persistent storage sparingly, and only for data that is "practicality" static.
Mutations
Mutations is a great way to create responsive UIs with so-called "optimistic updates". That is, to update the UI before the date is confirmed persisted on the server, like in the following example:
export function MyComponent() {
const { data, mutate } = useGameApi();
const toggleFavorite = useCallback(
(index) => {
// optimistically update the UI
mutate((draft) => {
draft.games[index].isFavorite = !draft.games[index].isFavorite;
});
// persist the change here or
// in the onMutation callback
},
[mutate]
);
return (
<ul>
{data.games.map((game) => (
<li key={game.id}>
{game.name}
<button onClick={}>{game.isFavorite ? '★' : '☆'}</button>
</li>
))}
</ul>
);
}