useQueryLoading saved progress…

useQuery

Build a custom hook that runs an asynchronous request and reports its progress to the component. Fetching data is never a single value — at any moment a request is either still in flight, finished with a result, or finished with a failure. useQuery(queryFn) takes a function that returns a Promise, runs it, and hands back { data, error, isLoading } so the component can render a spinner, the result, or an error message without wiring up the bookkeeping itself.

Signature

function useQuery<T>(queryFn: () => Promise<T>): {
  data: T | undefined;
  error: Error | undefined;
  isLoading: boolean;
};

The hook runs queryFn on mount and again whenever the queryFn reference changes. While a request is in flight, isLoading is true and both data and error are undefined.

Examples

function Profile({ userId }) {
  const fetchUser = useCallback(
    () => fetch(`/api/users/${userId}`).then((r) => r.json()),
    [userId],
  );
  const { data, error, isLoading } = useQuery(fetchUser);

  if (isLoading) return <Spinner />;
  if (error) return <p>Could not load: {error.message}</p>;
  return <h1>{data.name}</h1>;
}
// Lifecycle for a query that resolves:
// mount:    { data: undefined, error: undefined, isLoading: true }
// resolve:  { data: <result>,  error: undefined, isLoading: false }

// Lifecycle for a query that rejects:
// mount:    { data: undefined, error: undefined, isLoading: true }
// reject:   { data: undefined, error: <Error>,   isLoading: false }

Notes

  • Loading is the starting state. Before the promise settles, isLoading is true and there is no data or error yet.
  • Resolve and reject are mutually exclusive. A resolved query sets data and leaves error undefined; a rejected query sets error and leaves data undefined. Both flip isLoading to false.
  • Falsy results are real results. If the query resolves with 0, '', null, or false, that value is the data and loading is done. "Resolved with a falsy value" is not the same as "still loading."
  • Re-run on a new queryFn. When the queryFn reference changes, start a fresh request. Callers are expected to memoize queryFn (e.g. with useCallback) so it only changes when the underlying inputs change.
  • Guard against stale and unmounted updates. A promise from a superseded or unmounted request must not set state. Track an ignore flag so a late result is dropped instead of clobbering the current one or warning about an update after unmount.
Loading editor…