useApi

Simple fetch hook built on Zustand and Immer. Inspired by useSwr().

Install

yarn add @ntds/sticky

Basics

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 staleAfter is 2s

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 expireAfter is Infinity (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>
);
}

This guide is based on version: 1.1.0