Implement a useQuery
hook that manages a promise resolution which can be used to fetch data.
export default function Component({ param }) {const request = useQuery(async () => {const response = await getDataFromServer(param);return response.data;}, [param]);return (<div>{request.loading && <p>Loading...</p>}{request.error && <p>Error: {request.error.message}</p>}{request.data && <p>Data: {request.data}</p>}</div>);}
fn: () => Promise
: A function that returns a promisedeps: DependencyList
: An array of dependencies, similar to the second argument of useEffect
. Unlike useEffect
, this defaults to []
The hook returns an object that has different properties depending on the state of the promise.
status: 'loading'
: The promise is still pendingstatus: 'error'
: The promise was rejectederror: Error
: The error that caused the promise to be rejectedstatus: 'success'
: The promise was resolveddata
: The data resolved by the promise returned by fn
Implement a useQuery
hook that manages a promise resolution which can be used to fetch data.
export default function Component({ param }) {const request = useQuery(async () => {const response = await getDataFromServer(param);return response.data;}, [param]);return (<div>{request.loading && <p>Loading...</p>}{request.error && <p>Error: {request.error.message}</p>}{request.data && <p>Data: {request.data}</p>}</div>);}
fn: () => Promise
: A function that returns a promisedeps: DependencyList
: An array of dependencies, similar to the second argument of useEffect
. Unlike useEffect
, this defaults to []
The hook returns an object that has different properties depending on the state of the promise.
status: 'loading'
: The promise is still pendingstatus: 'error'
: The promise was rejectederror: Error
: The error that caused the promise to be rejectedstatus: 'success'
: The promise was resolveddata
: The data resolved by the promise returned by fn
The useQuery
hook can be implemented with useEffect
to begin the promise resolution and update the states accordingly.
The challenge here is to realize that promise resolutions are asynchronous from React updates, so there is a possibility of race conditions when the dependencies change before a pending promise is resolved.
To prevent this, we can use an ignore
flag to ignore the promise resolution if it is no longer relevant (e.g. deps have changed, the component has been unmounted). The ignore
is initialized within the function's closure; each time useEffect
runs, the function has its own ignore
instance variable and can refer to it when the promise is resolved and it has to decide whether to use the results.
This approach is well-documented in the React documentation.
import { DependencyList, useEffect, useState } from 'react';type AsyncState<T> =| { status: 'loading' }| { status: 'success'; data: T }| { status: 'error'; error: Error };export default function useQuery<T>(fn: () => Promise<T>,deps: DependencyList = [],): AsyncState<T> {const [state, setState] = useState<AsyncState<T>>({status: 'loading',});useEffect(() => {let ignore = false;setState({ status: 'loading' });fn().then((data) => {if (ignore) {return;}setState({ status: 'success', data });}).catch((error) => {if (ignore) {return;}setState({ status: 'error', error });});return () => {ignore = true;};}, deps);return state;}
console.log()
statements will appear here.