Learn React's key concurrent features—useTransition, useDeferredValue, Suspense, and useOptimistic—and how they coordinate to create smooth, responsive user experiences. Includes practical examples and best practices.
Aurora Scharff
August 19, 2025
Let's explore React's key concurrent features—useTransition()
, useDeferredValue()
, Suspense
, and useOptimistic()
—and how they work together to solve coordination challenges in modern applications.
These aren't just performance optimizations—they're coordination tools that help React prioritize urgent updates (like user input) over background work, creating smooth user experiences even under heavy workloads.
They enable a declarative approach to coordinating updates and async operations, improving code readability and maintainability.
React's concurrent rendering is what makes everything else possible. Instead of blocking the browser while rendering, React can pause, prioritize, and coordinate different types of work.
Before concurrent rendering, React worked synchronously. When you triggered an update, React would block the main thread until the entire component tree was re-rendered. This could cause janky interactions—typing might feel sluggish if expensive components were rendering simultaneously.
Concurrent rendering changes this by introducing interruptible rendering. React can start rendering an update, pause to handle more urgent work (like user input), then resume where it left off.
This foundation enables all the concurrent features we'll cover—each one uses this interruptible capability.
useTransition
marks state updates as non-urgent, allowing React to interrupt them for more important work. All state updates execute once the entire transition is complete.
React 19 introduces async transitions, allowing you to pass async functions directly to startTransition
. These async functions are called "Actions" and should be named accordingly to distinguish them from regular event handlers.
Here's the basic API:
const [isPending, startTransition] = useTransition();
It takes no parameters and returns an array with isPending
(a boolean indicating if a transition is active) and startTransition
(a function to mark updates as non-urgent).
Expensive computations shouldn't block user interactions—transitions help by deprioritizing heavy work.
Here's a tab button that uses a transition to prevent blocking during expensive tab changes:
function TabButton({ children, tabAction }) {
const [isPending, startTransition] = useTransition();
const handleTabChange = () => {
startTransition(() => tabAction());
};
return (
<button onClick={handleTabChange} style={{ opacity: isPending ? 0.7 : 1 }}>
{children}
</button>
);
}
Notice the naming here: the prop tabAction
follows the Action naming convention, signaling to the parent component that this is an Action callback.
For async operations like API calls, form submissions, and data mutations, transitions coordinate state updates with async operations, which prevents flickering UI. Here's a delete button that shows pending state while the server processes the request:
function DeleteButton({ itemId }) {
const [isPending, startTransition] = useTransition();
const handleDelete = () => {
startTransition(async () => {
await deleteItem(itemId);
});
};
return (
<button
onClick={handleDelete}
disabled={isPending}
style={{ opacity: isPending ? 0.7 : 1 }}
>
{isPending ? 'Deleting...' : 'Delete'}
</button>
);
}
Notice the isPending
state for the async function is provided automatically, avoiding manual loading states.
Suspense
provides declarative loading boundaries. It works with React.lazy()
for code splitting and activates with Suspense-enabled data sources—async Server Components, promises used with the use()
API, and libraries that support Suspense like React Query or SWR.
Here's the API:
<Suspense fallback={<LoadingSkeleton />}>
<Component />
</Suspense>
It takes a fallback
prop (JSX to show while loading) and children
that may suspend. It renders the fallback until all children are ready.
Component-level code splitting becomes declarative when combined with React.lazy
:
const LazyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<ComponentSkeleton />}>
<LazyComponent />
</Suspense>
);
}
With async data sources, you can wrap individual components in their own Suspense boundaries. Consider this dashboard:
function UserDashboard({ userId }) {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<UserPosts userId={userId} />
</Suspense>
</div>
);
}
Each component manages its own data while the parent declaratively handles loading states through Suspense boundaries.
Combining transitions with Suspense prevents jarring loading states during navigation. Here's a router that keeps the current page visible while the new page loads in the background:
function AppRouter() {
const [currentPage, setCurrentPage] = useState('home');
const [isPending, startTransition] = useTransition();
function navigateTo(page) {
startTransition(() => setCurrentPage(page));
}
return (
<div>
<nav>
{['home', 'profile', 'settings'].map(page => (
<button key={page} onClick={() => navigateTo(page)}>
{page}
</button>
))}
</nav>
<Suspense fallback={<PageSkeleton />}>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<PageContent page={currentPage} />
</div>
</Suspense>
</div>
);
}
In Next.js App Router, navigations are automatically wrapped in transitions under the hood.
Note: Transitions only "wait" long enough to avoid hiding already revealed content. They don't wait for nested Suspense boundaries.
The use
API is a utility that works well with Suspense for reading promises and context values. Unlike Hooks, it can be called conditionally and works with Suspense-enabled data sources or cached promises.
Basic API:
const data = use(promise);
const contextValue = use(Context);
It takes a promise or context and returns the resolved value. With promises, it suspends the component until resolution.
Here's a complete Suspense + use
API example:
function App() {
const userPromise = fetchUser('/api/user/123');
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <div><img src={user.avatar} /><h2>{user.name}</h2></div>;
}
The use
API suspends the UserProfile
component until the promise resolves, while Suspense shows the skeleton fallback during loading.
useDeferredValue
defers rendering parts of the UI that depend on frequently changing values, keeping current content visible until React has time to process new values.
Here's the API:
const deferredValue = useDeferredValue(value);
It takes a value and returns a deferred version that lags behind during rapid updates.
When user input triggers expensive filtering or processing, deferred values keep the input responsive:
function FilteredList({ items }) {
const [filter, setFilter] = useState('');
const deferredFilter = useDeferredValue(filter);
return (
<div>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<ExpensiveFilteredItems items={items} filter={deferredFilter} />
</div>
);
}
Wrap the expensive component in memo
to ensure it only re-renders when its props actually change:
const ExpensiveFilteredItems = memo(function ExpensiveFilteredItems({ items, filter }) {
return (
<div>
{items
.filter(item => item.name.toLowerCase().includes(filter.toLowerCase()))
.map(item => <div key={item.id}>{item.name}</div>)
}
</div>
);
});
Search interfaces benefit from deferred values combined with Suspense-enabled data sources. Suspense
provides a fallback on initial load, while the deferred value keeps previous results visible during typing, preventing jarring content flashes on every keystroke:
function SearchApp() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<Suspense fallback={<div>Searching...</div>}>
<SearchResults query={deferredQuery} />
</Suspense>
</div>
</div>
);
}
function SearchResults({ query }) {
if (!query) return <div>Start typing to search</div>;
const results = use(searchUsers(query));
return (
<div>
{results.map(user => <div key={user.id}>{user.name}</div>)}
</div>
);
}
The isStale
pattern shows users when results are updating by comparing the current query with the deferred version, using opacity to indicate when the search is catching up to their typing.
useOptimistic
shows optimistic updates immediately while the actual update happens in the background. It must be used within transitions for React to know how long the optimistic state should exist.
Here's the API:
const [optimisticState, addOptimistic] = useOptimistic(currentState, updateFn);
It takes the current state and an update function, then returns an array with the optimistic state and a function to trigger optimistic updates.
Here's a like button with optimistic updates:
function LikeButton({ post }) {
const [, startTransition] = useTransition();
const [optimisticPost, setOptimisticPost] = useOptimistic(
post,
(currentPost, newLiked) => ({ ...currentPost, liked: newLiked, likes: currentPost.likes + (newLiked ? 1 : -1) })
);
const toggleAction = () => {
startTransition(async () => {
setOptimisticPost(!optimisticPost.liked);
await updatePostLike(post.id, !optimisticPost.liked);
});
};
return (
<button onClick={toggleAction}>
{optimisticPost.liked ? '❤️' : '🤍'} {optimisticPost.likes}
</button>
);
}
The heart icon and count change instantly when clicked—no pending state needed since the optimistic update provides the feedback.
Forms benefit from optimistic updates to show immediate feedback while server processing happens in the background. This comment form uses React 19's form actions, which automatically wrap in transitions:
function CommentForm({ comments, onAddComment }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(currentComments, newComment) => [...currentComments, { ...newComment, isPosting: true }]
);
const formRef = useRef();
async function submitAction(formData) {
const comment = formData.get('comment');
addOptimisticComment({ text: comment, id: Date.now() });
await onAddComment(comment);
formRef.current.reset();
}
return (
<div>
<form ref={formRef} action={submitAction}>
<textarea name="comment" />
<button type="submit">Post</button>
</form>
{optimisticComments.map(c => (
<div key={c.id} style={{ opacity: c.isPosting ? 0.7 : 1 }}>
{c.text} {c.isPosting && '(posting...)'}
</div>
))}
</div>
);
}
When you submit the form, the optimistic comment is immediately added to the list with isPosting: true
. This property only exists while the form action transition is running - once the server request completes successfully, the optimistic comment is replaced by the real comment data (which doesn't have isPosting
). If the server request fails, useOptimistic
automatically rolls back and removes the optimistic comment entirely.
The concurrent features we've covered each solve specific coordination problems in modern applications:
useTransition()
creates lower priority state updates to keep user input responsive during heavy processing and async operations, with built-in loading stateuseDeferredValue()
defers rendering of UI parts that depend on frequently changing values, keeping interfaces responsive during heavy rendering and preventing jarring content flashes in async operationsSuspense
provides declarative loading boundaries for code splitting and async operations, coordinating server requests and loading statesuseOptimistic()
makes user interactions feel instant with optimistic updates for async operationsStart with the feature that addresses your specific challenge, then layer in others as your application grows more complex.
For a real-world example combining several of these patterns, check out Building an Async Combobox with useSuspenseQuery and useDeferredValue on my personal blog, which shows how useDeferredValue
and Suspense
coordinate in a complex search interface.
Sources:
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.
React Concurrent Features: An Overview
Learn React's key concurrent features—useTransition, useDeferredValue, Suspense, and useOptimistic—and how they coordinate to create smooth, responsive user experiences. Includes practical examples and best practices.
August 19, 2025
Aurora Scharff
React Children and cloneElement: Component Patterns from the Docs
Explore React's Children utilities and cloneElement API through their excellent documentation. Learn about compound component patterns, understand their limitations, and discover why modern alternatives like render props and context are often better choices for component composition.
August 6, 2025
Aurora Scharff
Structuring State in React: 5 Essential Patterns
Learn 5 essential patterns for structuring React state effectively. Discover how to group related data, avoid contradictions, eliminate redundancy, and keep your components maintainable and bug-free.
July 23, 2025
Aurora Scharff
We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.
Let us help you get the resources you need.