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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
