How to Solve Async Race Conditions and Caching in React with Custom Hooks

This article explains why React's synchronous component model struggles with async data fetching, demonstrates how to cancel stale requests, introduces a refetch‑only hook, and shows how to implement caching and deduplication to make queries pure functions, while recommending React Query and SWR for production use.

Huolala Tech
Huolala Tech
Huolala Tech
How to Solve Async Race Conditions and Caching in React with Custom Hooks

Background

React promotes the core idea fn(state) = UI , meaning a deterministic function produces UI from a given state. However, components (the fn) can only run synchronous code, so there is no built‑in concept of async fn(state) = UI , which makes asynchronous programming cumbersome.

Async Timing Issue

Typical data fetching in React uses useEffect with the browser fetch API. The example below defines a useRequest hook that tracks data and isLoading and triggers the request on mount.

function useRequest(queryFn, deps) {
  const [data, setData] = React.useState();
  const [isLoading, setIsLoading] = React.useState(false);

  const run = () => {
    // ...
    queryFn(...args)
      .then((data) => {
        setData(data);
      })
      .catch(() => {
        setData(undefined);
      })
      .finally(() => {
        setIsLoading(false);
      });
  };

  React.useEffect(() => {
    run();
  }, deps);

  return { data, isLoading, run };
}

When deps change frequently or run is called manually, the asynchronous nature of run can cause race conditions: the displayed data may not correspond to the latest deps.

Attempted Solution

One idea is to cancel the previous request whenever deps change, ensuring only the last request takes effect. The hook is rewritten to wrap the promise in a cancelable object and replace run with a side‑effect‑free refetch method.

// Wrap request function as a cancelable Promise
const makeCancelable = (promise) => { ... };

function useRequest(queryFn, deps) {
  // ...
  const run = () => {
    const wrappedPromise = makeCancelable(queryFn());
    wrappedPromise.then().catch().finally();
    return wrappedPromise.cancel;
  };

  React.useEffect(() => {
    return run();
  }, deps);

  return { data, isLoading, run };
}

Even with cancellation, directly invoking run still leads to timing issues, so the hook is renamed to useQuery and the exposed method becomes a pure refetch that only triggers when no request is in progress.

function useQuery(queryFn, deps) {
  // ...
  const refetch = () => {
    if (!isLoading) {
      run();
    }
  };
  return { data, isLoading, refetch };
}

Caching and Deduplication

To share results across components, deps (renamed to queryKey) is used as a cache key. A global queries map stores query instances; identical queryKey values return the same cached data and listeners, eliminating duplicate network calls.

const queries = {};
function getQuery(queryKey, queryFn) {
  const hash = computeHash(queryKey);
  if (queries[hash]) return queries[hash];
  queries[hash] = {
    isLoading: false,
    data: undefined,
    listeners: new Set(),
    refetch() { /* ... */ },
  };
  return queries[hash];
}

function useQuery(queryKey, queryFn) {
  const [, forceUpdate] = React.useReducer((n) => n + 1, 0);
  const currentQuery = getQuery(queryKey, queryFn);
  React.useEffect(() => {
    currentQuery.refetch();
    currentQuery.listeners.add(forceUpdate);
    return () => currentQuery.listeners.delete(forceUpdate);
  }, [currentQuery]);
  return { data: currentQuery.data, isLoading: currentQuery.isLoading, refetch: currentQuery.refetch };
}

With this approach, identical queryKey values cause a single request, all components share the cached result, and rapid parameter changes never produce stale UI.

Request Library Recommendation

For production projects, the article recommends using established libraries such as React Query and SWR , which implement caching, deduplication, and synchronization out of the box and can even replace global state management solutions.

Side Note

The React RFC "First class support for promises and async/await" proposes a new use API to address async component rendering ( async fn(state) = UI ), which may be of interest for future implementations.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ReactcachingdeduplicationAsyncuseEffectCustom Hook
Huolala Tech
Written by

Huolala Tech

Technology reshapes logistics

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.