3 minutes
Diving Into React’s Concurrent Features
Summary
React v18’s concurrent features allow React to prepare and prioritize work without blocking the main thread. Automatic batching, the startTransition
API, useDeferredValue
, and useTransition
help you build smooth, responsive UIs by grouping updates and deferring low-priority work until later (reactjs.org, blog.logrocket.com).
1. Introduction
Traditional React renders block until all work completes, potentially causing frame drops. Concurrent rendering in v18 breaks rendering into interruptible units, allowing React to yield to the browser for urgent tasks like user input, resulting in jank-free experiences.
2. Automatic Batching
2.1 What Changed
- Pre-v18: React batched state updates only inside React event handlers. Updates in timeouts or native events triggered multiple renders.
- v18+: React batches updates across all contexts (timeouts, promise callbacks, native events).
2.2 Impact Example
// Before v18
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
});
// This would cause two re-renders.
// In v18
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
});
// Now, one re-render occurs due to automatic batching.
Batching reduces unnecessary work and boosts performance, especially with frequent or asynchronous updates.
3. startTransition
3.1 Purpose
Use startTransition
to mark non-urgent updates (e.g., fetching filtered list) as a transition, so React can prioritize urgent updates (like keystrokes) first.
3.2 Basic Usage
import { startTransition } from 'react';
function onSearchChange(e) {
const value = e.target.value;
setQuery(value); // urgent
startTransition(() => {
setFilteredItems(processItems(value)); // non-urgent
});
}
Users typing won’t lag as filtering work is deferred.
4. useDeferredValue
4.1 Purpose
useDeferredValue
returns a deferred version of a value that lags behind when React is busy, useful for expensive renders.
4.2 Example
import { useDeferredValue } from 'react';
function SearchResults({ query, items }) {
const deferredQuery = useDeferredValue(query);
const filtered = items.filter(item => item.includes(deferredQuery));
return <List data={filtered} />;
}
The UI responds instantly to typing (showing previous results) while heavy filtering happens in the background.
5. useTransition
5.1 Purpose & API
useTransition
returns [isPending, startTransition]
, letting you show a pending state during transitions.
5.2 Example
import { useTransition } from 'react';
function App() {
const [isPending, startTransition] = useTransition();
const [items, setItems] = useState([]);
function handleClick() {
startTransition(() => {
setItems(heavyComputation());
});
}
return (
<>
<button onClick={handleClick}>Load</button>
{isPending ? <Spinner /> : <ItemList items={items} />}
</>
);
}
isPending
lets you show a loader only for deferred updates.
6. Concurrency & Suspense Integration
Concurrent features underpin client-side Suspense: transitions and deferred values decide when to reveal or hide fallback UIs, creating seamless data-driven experiences. For example, wrapping data-fetching in a startTransition
can coordinate with <Suspense>
boundaries to avoid flicker.
7. Pitfalls & Best Practices
- Avoid Over-Deferring: Don’t wrap all updates in transitions, keep urgent feedback immediate.
- Debugging: Enable React DevTools’s concurrent rendering flag to visualize transitions and renders.
- Boundary Strategy: Use deferred values and transitions around clear UI sections (e.g., search panels, dashboards).
8. Full Example Walkthrough
Build a live search:
function SearchComponent({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const filtered = useMemo(
() => items.filter(i => i.includes(deferredQuery)),
[items, deferredQuery]
);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value);
startTransition(() => {
// expensive state update deferred
setItems(filtered);
});
}
return (
<div>
<input onChange={handleChange} value={query} />
{isPending ? <Spinner /> : <List data={filtered} />}
</div>
);
}
- Instant input responsiveness via
setQuery
. - Deferred filtering with
useDeferredValue
andstartTransition
. - Loader display with
isPending
.
9. Conclusion & Further Reading
React v18’s concurrent features significantly improve UI responsiveness by batching updates and deferring non-critical work. Start using startTransition
, useDeferredValue
, and useTransition
in appropriate scenarios to enhance user experience without rewriting core logic.
Further Reading
- React Concurrent Mode Intro: https://reactjs.org/docs/concurrent-mode-intro.html
- LogRocket Blog on React 18: https://blog.logrocket.com/react-18-concurrent-features/
- React RFCs: https://github.com/reactjs/rfcs