Skip to content

Instantly share code, notes, and snippets.

@lski
Created February 7, 2021 12:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lski/a502241f4e5f4a4e32f5215f4b44d8af to your computer and use it in GitHub Desktop.
Save lski/a502241f4e5f4a4e32f5215f4b44d8af to your computer and use it in GitHub Desktop.
A basic load data React hook that handles using fetch to get data
import { useEffect, DependencyList, useState } from 'react';
/**
* `useLoadData` wraps loading data via fetch in as a hook.
*
* Supports handling a loading and error state for the current component and handles
* cleanup using a useEffect via an AbortSignal to prevent updates happening on
* unmounted components. To control how often it runs, use the dependancy list parameter,
* see useEffect for more details.
*
* @example <caption>Basic usage</caption>
*
* const [data, loading, error] = useLoadData((signal) =>
* fetch('a-url', { signal })
* .then(response => {
* if(!response.ok) {
* throw new Error(response.statusText);
* }
*
* return response.json() as Promise<{ name: string }>;
* }));
*
* @example <caption>Because its in the dependency list each time id is changed it will reload (re-run) the fetch and update the data.</caption>
*
* const [id, setId] = useState(1);
*
* const [data, loading, error] = useLoadData((signal) =>
* fetch(`a-url/${id}`, { signal })
* .then(response => {
* if (!response.ok) {
* throw new Error(response.statusText);
* }
* return response.text();
* }), [id]);
*
* @param request A function that accepts an abort signal and returns a promise that either resolves or rejects.
*
* @param dependencies Optional dependencies (see useEffect for more details), by default only runs once
*/
export const useLoadData = <T extends unknown>(request: (signal: AbortSignal) => Promise<T>, dependencies: DependencyList = []) : [T | null, boolean, string | null] => {
interface State {
data: null | T;
error: null | string;
loading: boolean;
}
const [state, setState] = useState<State>({
data: null,
error: null,
loading: true,
});
useEffect(
() => {
setState(state => ({
data: state.data,
error: null,
loading: true,
}));
const controller = new AbortController();
// Before changing state on the request being returned/throw
// ensure its not be aborted first which causes mounting errors
request(controller.signal)
.then((data) => {
if (!controller.signal.aborted) {
setState({
data: data,
error: null,
loading: false,
});
}
})
.catch((e: Error | string) => {
if (!controller.signal.aborted) {
setState(state => ({
data: state.data,
error: typeof e === 'string' ? e : e.message,
loading: false,
}));
}
});
return () => {
controller.abort();
};
},
dependencies === null ? undefined : dependencies
);
return [state.data, state.loading, state.error];
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment