Understanding React Concurrent Rendering with Examples
#react
#webdev
#concurrency
#tutorial
Introduction
React’s concurrent rendering is a set of features that helps keep your app responsive even when work gets heavy. It does this by interrupting rendering, prioritizing updates, and allowing UI to remain interactive while other work continues in the background. In this post, we’ll walk through the core concepts and show practical examples that illustrate how to use concurrent rendering without sacrificing correctness or user experience.
How concurrent rendering works at a high level
- Time slicing: React breaks work into units that can be paused and resumed, so the UI stays responsive.
- Interruptible rendering: Lower-priority updates can be interrupted by higher-priority interactions (like typing or clicking).
- Priority-based scheduling: React attempts to render higher-priority updates first.
- Opt-in by features: Concurrency in React 18+ is opt-in for parts of the UI via hooks and new rendering APIs.
These concepts enable smoother interactions, such as responsive search, large lists, and progressive loading, even when some work would traditionally block the main thread.
Core tools you can use
- useTransition: Offloads lower-priority updates to keep interactions snappy.
- startTransition: A helper to wrap state updates that can be deferred without blocking user input.
- useDeferredValue: Defer a value to reduce the amount of UI that needs to update immediately.
- Suspense: Coordinate asynchronous data fetching or code loading with fallback UI.
- React.lazy: Lazy-load components to improve perceived performance.
Example 1: Using useTransition for a responsive search filter
This example shows how to keep the input field responsive while filtering a large list. The filtering work is wrapped in a transition so React can keep the UI interactive during the update.
import React, { useState, useTransition } from 'react';
function SearchList({ items }) {
const [query, setQuery] = useState('');
const [filtered, setFiltered] = useState(items);
const [isPending, startTransition] = useTransition();
const onChange = (e) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
// Simulate or perform a potentially heavier computation
const newFiltered = items.filter((item) =>
item.toLowerCase().includes(value.toLowerCase())
);
setFiltered(newFiltered);
});
};
return (
<div>
<input value={query} onChange={onChange} placeholder="Search items…" />
{isPending && <span>Updating results...</span>}
<ul>
{filtered.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
Notes:
- The input remains responsive while the results update.
- UseTransition marks the update as low-priority so typing doesn’t block rendering.
Example 2: Using useDeferredValue to defer heavy renders
useDeferredValue lets you render with a deferred value to avoid stalling the UI when a user is typing quickly. The visible results update with a slight delay, preserving interactivity.
import React, { useState, useDeferredValue } from 'react';
function LargeListFilter({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const visible = items.filter((item) =>
item.toLowerCase().includes(deferredQuery.toLowerCase())
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Type to search..."
/>
<ul>
{visible.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
{visible.length === 0 && <div>No results</div>}
</div>
);
}
Notes:
- The input updates immediately, but the filtering uses a deferred value, helping keep the UI responsive during rapid typing.
- useDeferredValue is especially helpful when rendering a large list or performing expensive computations in render.
Example 3: Suspense, lazy loading, and concurrent rendering
Suspense enables a smooth loading experience for asynchronous data or code loading. When combined with lazy-loaded components, it helps keep the shell responsive while the heavy pieces load in the background.
import React, { Suspense, lazy } from 'react';
const HeavyWidget = lazy(() => import('./HeavyWidget'));
function App() {
return (
<div>
<h1>Concurrent Rendering Demo</h1>
<Suspense fallback={<div>Loading widget...</div>}>
<HeavyWidget />
</Suspense>
</div>
);
}
Notes:
- HeavyWidget will render only after its code chunk is loaded, without blocking the rest of the UI.
- Suspense is a key part of building smooth experiences with concurrent rendering in React 18+.
Practical guidance and best practices
- Start with small, safe components: Prefer keeping components as pure and deterministic as possible.
- Avoid long-running synchronous work in render: If a compute is expensive, move it to useMemo, or perform it asynchronously and reflect results via state.
- Combine patterns thoughtfully: Use useTransition for interactive updates, useDeferredValue for expensive lists, and Suspense for data loading and code splitting.
- Measure performance: Use React DevTools Profiler to observe how concurrent features affect rendering, frames, and responsiveness.
- Be mindful of side effects: Ensure effects are not tied to rendering order in ways that could cause inconsistent UI during interruptions.
What to watch out for
- Not every component benefits from concurrency. If updates are already fast, adding complexity can hurt readability.
- Data fetching libraries and components must be compatible with Suspense to fully reap its benefits.
- Debugging concurrent rendering can be trickier; ensure you test realistic user interactions and edge cases.
Conclusion
React concurrent rendering provides powerful tools to keep UIs responsive under load. By selectively using useTransition, useDeferredValue, Suspense, and lazy loading, you can craft smoother, more fluid experiences without compromising correctness. Start small, measure, and gradually adopt these patterns where they make a tangible difference.
Further resources
- React 18 and beyond: getting started with concurrent rendering patterns
- useTransition and startTransition patterns in real apps
- Suspense for data fetching and code splitting
- Performance profiling in React DevTools to see the impact of concurrency features