Skip to content

Instantly share code, notes, and snippets.

@drbr
Last active December 19, 2020 03:35
Show Gist options
  • Save drbr/819c9df272bfba1b2397c4e3e296f5ab to your computer and use it in GitHub Desktop.
Save drbr/819c9df272bfba1b2397c4e3e296f5ab to your computer and use it in GitHub Desktop.
Hook to fetch async data, a cheap knockoff of react-query
import * as React from 'react';
export type AsyncFetchState<T, E = unknown> = {
data: T | null;
loading: boolean;
error: E | undefined;
};
/**
* This hook performs an async operation and stores its result in component state. It protects
* against the common pitfalls of trying to set state on an unmounted or stale component.
*
* This hook is okay for simple cases, but for more complex use cases, consider using a more robust
* library such as react-query or apollo.
*
* The hook returns the following information:
* - `data`: the most recent valid (successfully-fetched) data. Will be null before first fetch.
* _This will still contain the last valid data_ during subsequent loading states or when the most
* recent fetch returned an error, even though that data no longer corresponds to the current
* params.
* - `loading`: a boolean that will be true while a fetch is in progress (either on initial load
* or during a refetch after the params change)
* - `error`: if the most recent invocation of the fetch returned an error, it will be populated
* here.
* It returns two things: the fetched result (will be null before first fetch returns),
* and `loading`, which will be true whenever a fetch is in progress (this will happen on the
* inital load, as well as during a refetch after the params change).
*
* @param fetcher
* @param params
*/
export function useAsyncFetch<P extends unknown[], T, E = unknown>(
fetcher: (...params: P) => PromiseLike<T>,
...params: P
): AsyncFetchState<T, E> {
const [state, dispatch] = React.useReducer<typeof asyncFetchReducer>(
asyncFetchReducer,
{ loading: true, data: null, error: undefined }
);
React.useEffect(
() => {
// The effect will be initiated only when the component is under valid conditions (e.g. rendered).
// Once the effect becomes stale (either via unmount or params change), `queryStatus` gets set
// to `stale` in the cleanup. A stale query status signifies that the fetch result should be
// discarded.
let queryStatus: 'valid' | 'stale' = 'valid';
async function fetchTheData() {
dispatch({ type: 'START_FETCH' });
try {
const data = await fetcher(...params);
if (queryStatus === 'valid') {
dispatch({ type: 'FETCH_SUCCESSFUL', data });
}
} catch (e) {
if (queryStatus === 'valid') {
dispatch({ type: 'FETCH_FAILED', error: e });
}
}
}
fetchTheData();
return () => {
queryStatus = 'stale'
};
},
[fetcher, ...params]
);
return state as AsyncFetchState<T, E>;
}
type AsyncFetchAction<T, E = unknown> =
| { type: 'START_FETCH' }
| { type: 'FETCH_SUCCESSFUL', data: T }
| { type: 'FETCH_FAILED', error: E | undefined }
function asyncFetchReducer<T, E = unknown>(
prevState: AsyncFetchState<T, E>,
action: AsyncFetchAction<T, E>
): AsyncFetchState<T, E> {
switch (action.type) {
case 'START_FETCH': return {
loading: true,
data: prevState.data,
error: prevState.error,
};
case 'FETCH_SUCCESSFUL': return {
loading: false,
data: action.data,
error: undefined,
};
case 'FETCH_FAILED': return {
loading: false,
data: prevState.data,
error: action.error,
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment