React Concurrent Features Explained — useTransition, useDeferredValue, and Suspense
In this tutorial, you'll learn about React Concurrent Features Explained. We cover key concepts, practical examples, and best practices.
React Concurrent Features let you mark some state updates as non-urgent so the browser stays responsive to user input while heavy rendering happens in the background.
What You'll Learn
Build responsive React UIs using concurrent features — useTransition for non-blocking state updates, useDeferredValue for deferring derived values, and useSyncExternalStore for concurrent-safe external subscriptions.
Why It Matters
Without concurrency, every state update blocks the main thread until rendering finishes. A heavy list filter that takes 50ms causes visible input lag. Concurrent features interrupt long renders to process input first, then resume rendering. This cuts perceived latency by 60-80% in data-heavy UIs.
Real-World Use
A searchable product catalog with 10,000 items filters the list as the user types. Without concurrency, each keystroke lags as the list re-renders. With useTransition, keystrokes are processed instantly and the filtered list renders in the background. Doda Browser's extension manager uses this — typing in the search box stays smooth while filtering hundreds of extensions.
useTransition — Non-Blocking State Updates
Wrap non-urgent state updates in startTransition to let React interrupt them for more urgent updates:
import { useState, useTransition } from "react";
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
export default function SearchPage() {
const [query, setQuery] = useState("");
const [filtered, setFiltered] = useState(items);
const [isPending, startTransition] = useTransition();
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setQuery(value); // urgent: update the input value immediately
startTransition(() => {
// non-urgent: filter the list — React can interrupt this
setFiltered(
items.filter(item =>
item.toLowerCase().includes(value.toLowerCase())
)
);
});
}
return (
<div style={{ maxWidth: 600, margin: "2rem auto" }}>
<input
value={query}
onChange={handleChange}
placeholder="Search 10,000 items..."
style={{ width: "100%", padding: 12, fontSize: 16, border: "1px solid #ccc", borderRadius: 4 }}
/>
{isPending && <p style={{ color: "#888" }}>Updating list...</p>}
<ul style={{ maxHeight: 400, overflow: "auto", marginTop: 12 }}>
{filtered.map(item => (
<li key={item} style={{ padding: "4px 0" }}>{item}</li>
))}
</ul>
</div>
);
}
Expected output: Typing in the input is always responsive — no lag. The filtered list updates asynchronously. React interrupts the list filtering each time a new keystroke arrives, ensuring the input never freezes. isPending shows a visual indicator when the list is stale.
useDeferredValue — Deferring Derived Values
useDeferredValue is a simpler alternative to useTransition when you cannot control where the state is set:
import { useState, useDeferredValue, useMemo } from "react";
const allPosts = Array.from({ length: 5000 }, (_, i) => ({
id: i,
title: `Post ${i + 1}`,
content: `Content of post ${i + 1}`,
}));
export default function BlogSearch() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const filteredPosts = useMemo(
() => allPosts.filter(post =>
post.title.toLowerCase().includes(deferredQuery.toLowerCase())
),
[deferredQuery]
);
return (
<div style={{ maxWidth: 600, margin: "2rem auto" }}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search posts..."
style={{ width: "100%", padding: 12, fontSize: 16, border: "1px solid #ccc", borderRadius: 4 }}
/>
<div style={{ opacity: isStale ? 0.5 : 1, transition: "opacity 0.2s", marginTop: 12 }}>
{filteredPosts.map(post => (
<article key={post.id} style={{ padding: "8px 0", borderBottom: "1px solid #eee" }}>
<h3 style={{ margin: 0 }}>{post.title}</h3>
<p style={{ margin: "4px 0 0", color: "#555" }}>{post.content}</p>
</article>
))}
</div>
</div>
);
}
Expected output: deferredQuery lags behind query by one render cycle. The input stays responsive while the filtered list renders with the deferred (older) value. isStale dims the old list visually, providing a smooth transition instead of a blank loading state.
useSyncExternalStore — Concurrent-Safe External Stores
When using external state (Redux, Zustand, global stores), useSyncExternalStore ensures correct behavior during concurrent rendering:
import { useSyncExternalStore } from "react";
// External store (could be Zustand, Redux, or a plain object)
let counter = 0;
const listeners = new Set<() => void>();
const store = {
getSnapshot: () => counter,
subscribe: (callback: () => void) => {
listeners.add(callback);
return () => listeners.delete(callback);
},
};
export function increment() {
counter++;
listeners.forEach(l => l());
}
export function useCounter() {
return useSyncExternalStore(store.subscribe, store.getSnapshot);
}
// Usage in a component
function CounterDisplay() {
const count = useCounter();
return (
<div style={{ textAlign: "center", padding: "2rem" }}>
<h2>Counter: {count}</h2>
<button
onClick={increment}
style={{ padding: "8px 16px", fontSize: 18, cursor: "pointer" }}
>
Increment
</button>
</div>
);
}
Expected output: useSyncExternalStore subscribes to the external store and returns the current snapshot. During concurrent rendering, React uses getSnapshot to ensure it reads a consistent value. If the store changes during a render, React detects the tear and re-renders synchronously. Without this, concurrent rendering could show inconsistent state.
Concurrent Rendering with Suspense
Combine concurrent features with Suspense for interruption-safe data loading:
import { Suspense, useTransition } from "react";
import { createResource } from "./data";
const resource = createResource();
function SlowProfile() {
const data = resource.read();
return (
<div style={{ padding: "1rem", border: "1px solid #ddd", borderRadius: 8 }}>
<h3>{data.name}</h3>
<p>{data.bio}</p>
</div>
);
}
export default function ProfilePage() {
const [isPending, startTransition] = useTransition();
function refresh() {
startTransition(() => {
resource.refetch();
});
}
return (
<div>
<button
onClick={refresh}
disabled={isPending}
style={{
padding: "8px 16px",
background: isPending ? "#ccc" : "#1976d2",
color: "#fff",
border: "none",
borderRadius: 4,
cursor: isPending ? "not-allowed" : "pointer",
marginBottom: 16,
}}
>
{isPending ? "Refreshing..." : "Refresh Profile"}
</button>
<Suspense fallback={<div>Loading profile...</div>}>
<SlowProfile />
</Suspense>
</div>
);
}
Expected output: Clicking "Refresh Profile" triggers startTransition, which tells React the Suspense fallback reveal is low priority. If the data loads fast enough (within a timeout), React shows the new data directly without flashing the fallback. If loading is slow, the fallback appears but user interactions remain responsive.
Common Errors
Practice Questions
What is the difference between
useTransitionanduseDeferredValue?useTransitionwraps state updates, letting you control which updates are urgent vs non-urgent from the setter side.useDeferredValueworks on the consumer side — it defers a value derived from urgent state, useful when you cannot control where the state is set.How does
useSyncExternalStoreprevent "tearing" during concurrent rendering? Tearing occurs when components see different versions of external state during the same render.useSyncExternalStorecallsgetSnapshotto capture a consistent value. If the store changes mid-render, React detects the mismatch and re-renders synchronously.When does
isPendingfromuseTransitionreturntrue? When a transition started but has not yet completed. This happens when the state update triggers a Suspense boundary (data fetching) or when the deferred render is still in progress. It returnsfalseonce the new UI has committed.
Challenge
Build a live search UI that queries 20,000 records. Use useTransition for input responsiveness and a simulated network delay. Show the stale result set while the new results are loading. Add a pending indicator that appears only if the transition takes longer than 200ms. Compare input responsiveness with and without concurrency.
Real-World Task
Find a component in your app that feels sluggish during input (search, filter, or sort). Profile it with React DevTools. Wrap the filtering state update in startTransition. If the data comes from props or context, use useDeferredValue instead. Measure the input response time before and after using the Performance tab in DevTools.
Mini Project: Product Catalog with Concurrent Search
Build a product catalog with:
- 10,000 product items loaded from a simulated API
- Real-time search with
useTransitionfor non-blocking filtering - Image thumbnails that lazy-load using Suspense
- A "sort by" dropdown that triggers with
useDeferredValue - Concurrent-safe external store for cart state using
useSyncExternalStore
Profile the input latency and compare with and without concurrent features.
Learning Path
flowchart LR A[React Suspense & Streaming SSR] --> B[React Compiler Explained] B --> C[React Concurrent Features] C --> D[React Server Actions Guide] D --> E[React Query & TanStack Query] style C fill:#4a90d9,color:#fff
Next Steps
- Previous: React Suspense & Streaming SSR Explained, React Compiler Explained
- Next: React Server Actions Guide, React Context Performance Patterns
- Related: React Performance Optimization — Memoization, Lazy Loading, and Profiling, React State Management — Redux, Zustand, Context API Compared
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Doda Browser's extension manager uses useTransition to keep the search input responsive while filtering hundreds of installed extensions — keystrokes never lag, even when the extension list has complex metadata to filter through.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro