Data fetching patterns in React: waterfalls, parallelism and caching
Most slow React pages are not slow because of rendering. They are slow because six requests are queued behind each other for no reason.
The most damaging React hooks mistakes are using effects to synchronise state that could be derived, capturing stale values in closures, omitting cleanup for subscriptions and timers, and creating unstable dependencies that make effects run on every render. Each has a specific fix that removes the effect rather than patching it.
If a value can be calculated from props or state, calculate it during render. Storing it in state and updating it in an effect adds a render pass, a source of truth, and a chance for the two to disagree.
// Wrong: two renders, two sources of truth
const [total, setTotal] = useState(0);
useEffect(() => setTotal(items.reduce((s, i) => s + i.price, 0)), [items]);
// Right: one render, one source of truth
const total = items.reduce((sum, item) => sum + item.price, 0);A callback registered once captures the values that existed at that moment. An interval created in an effect with an empty dependency array will forever see the first render's state — a bug that only appears once the value actually changes.
// Wrong: count is always 0 inside the interval
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []);
// Right: the updater form does not capture count
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 1000);
return () => clearInterval(id);
}, []);In a single-page app the cost compounds: navigate between two routes twenty times and you have twenty live listeners. Users report 'it gets slow after a while', which is the hardest kind of bug to reproduce in a five-minute QA pass.
Two in-flight requests can resolve out of order, leaving the UI showing results for a query the user has already changed.
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then((res) => res.json())
.then(setResults)
.catch((err) => { if (err.name !== "AbortError") setError(err); });
return () => controller.abort();
}, [query]);No, but it is overused. It is the right tool for synchronising with external systems and the wrong tool for deriving values, transforming props or reacting to user events.
When you need to measure the DOM and change it before the browser paints — tooltip positioning, scroll restoration. It blocks paint, so use it sparingly and never for data fetching.
Because the effect uses it and it could change between renders. Either define it inside the effect, wrap it in useCallback, or move it out of the component if it has no dependencies.
It handles memoisation-related instability, which removes some dependency churn. It does not remove unnecessary effects, add cleanup, or fix race conditions — those are design problems.
Harshal Patel
Founder & Lead Engineer, ROVQIX
Harshal leads engineering at ROVQIX, where he has shipped production Next.js, Node.js and PostgreSQL systems for startups, SaaS teams and ecommerce brands. He writes about the trade-offs behind architecture decisions rather than the framework of the week.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
Most slow React pages are not slow because of rendering. They are slow because six requests are queued behind each other for no reason.
Most 'state management' debates are category errors. Server data and UI state are different problems and need different tools.
A test suite nobody trusts is worse than none — it costs time and provides false confidence. Here is what we actually test on client projects.
No spam. Just the occasional case study and craft breakdown.