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]);if (request.status === 'loading') {return <p>Loading...</p>;}if (request.status === 'error') {return <p>Error: {request.error.message}</p>;}return <p>Data: {request.data}</p>;}
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 fnuseQuery exposes a small async state machine for the current dependency set: it returns { status: 'loading' }, then eventually { status: 'success', data } or { status: 'error', error }. useState stores that discriminated union, and useEffect owns the lifecycle of each request run.
The main correctness concern is stale async work. A request started for old dependencies can resolve after a newer request has already begun. The effect-local ignore flag fixes that: cleanup marks the current run as obsolete, and outdated promise handlers return early instead of overwriting state.
No ref is necessary here because each request belongs to one specific effect run. When deps change, React tears down the old run, starts a new one, resets the state to loading, and invokes fn() again.
The returned state is a discriminated union keyed by status, so consumers should branch on request.status before accessing data or error.
This approach is well-documented in the React documentation.
import { useEffect, useState } from 'react';/*** @template T* @param {() => Promise<T>} fn* @param {import("react").DependencyList} deps*/export default function useQuery(fn, deps = []) {const [state, setState] = useState({status: 'loading',});useEffect(() => {// Ignore late promise resolutions after deps change or the component unmounts.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;}
This question is about safe effect-driven async state, not a full data-fetching library. Caching, retries, deduplication, and background revalidation are intentionally out of scope.
console.log() statements will appear here.