Created
June 10, 2022 16:49
-
-
Save zachfedor/782a3b0b8305fdd632cee2feb29087ac to your computer and use it in GitHub Desktop.
React Async Hook
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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