Skip to content

Instantly share code, notes, and snippets.

@zachfedor
Created June 10, 2022 16:49
Show Gist options
  • Save zachfedor/782a3b0b8305fdd632cee2feb29087ac to your computer and use it in GitHub Desktop.
Save zachfedor/782a3b0b8305fdd632cee2feb29087ac to your computer and use it in GitHub Desktop.
React Async Hook
/**
* Hook to create safe useReducer dispatch function
*
* This only runs a dispatch if the component is still mounted, otherwise it
* returns undefined to prevent memory leaks from async functions, for example.
*/
const useSafeDispatch = <T>(dispatch: React.Dispatch<T>) => {
// The component is not rendered yet when hook is first called in the component logic
const isMounted = React.useRef(false);
React.useLayoutEffect(() => {
// Now that a layout effect has run, the component has been mounted into the DOM
isMounted.current = true;
// This cleanup function will run when the component is unmounted
return () => {
isMounted.current = false;
};
}, []);
// Return a safe dispatch function
return React.useCallback(
(action) => (isMounted.current ? dispatch(action) : undefined),
[dispatch]
);
};
type UseAsyncState<TData, TError> = {
status: "idle" | "loading" | "error" | "success";
data: TData | null;
error: TError | null;
};
/**
* Hook to track async function state and result
*
* Similar to react-query's useQuery hook, but returns a `run` function that accepts
* any Promise which resolves into `data` or rejects into `error`. For example:
* ```
* const { data, error, status, run } = useAsync();
* const onClick = () => run(fetchUser("foo"));
* ```
*/
const useAsync = <TData, TError = Error>(
initialState: Partial<UseAsyncState<TData, TError>> = {}
) => {
const initialStateRef = React.useRef<UseAsyncState<TData, TError>>({
status: "idle",
data: null,
error: null,
...initialState,
});
const [{ status, data, error }, dispatch] = React.useReducer(
// Simple reducer to merge new values from action into existing state
(
state: UseAsyncState<TData, TError>,
action: Partial<UseAsyncState<TData, TError>>
) => ({ ...state, ...action }),
initialStateRef.current
);
const safeDispatch = useSafeDispatch(dispatch);
const setData = React.useCallback(
(_data) => safeDispatch({ data: _data, status: "success", error: null }),
[safeDispatch]
);
const setError = React.useCallback(
(_error) => safeDispatch({ error: _error, status: "error" }),
[safeDispatch]
);
// Wrap the async function call and handle setting status and result/error.
const run = React.useCallback(
(promise: Promise<TData>) => {
safeDispatch({ status: "loading" });
return promise
.then((result: TData) => {
setData(result);
return result;
})
.catch((err) => {
setError(err);
return err;
});
},
[safeDispatch, setData, setError]
);
return {
run,
data,
error,
status,
isIdle: status === "idle",
isLoading: status === "loading",
isSuccess: status === "success",
isError: status === "error",
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment