How useDeferredValue and useTransition Work (With Real Examples)

React 18 introduced concurrent rendering — the ability to interrupt, pause, and prioritise render work so that urgent updates don't get blocked by expensive ones. useDeferredValue and useTransition are the two hooks that give you access to this from inside your components.
They solve the same category of problem: a state update that triggers an expensive render is making the UI feel sluggish. But they approach it differently, and using the wrong one for the job produces code that's harder to understand without solving the actual problem.
Let's start with the problem they both address.
The sluggish UI problem
You have a search input. As the user types, you filter a large list. The filtering is expensive — say, 2000 items with a complex matching function. Every keystroke triggers a re-render that takes 80ms. The input feels laggy because React is busy re-rendering the list before it can update the input.
The input update and the list render are treated with equal priority. React finishes the expensive list render before it can process the next keystroke. The result: a UI that feels like it's fighting you.
Both hooks fix this. But they do it at different layers.
useTransition: wrap the state update
useTransition lets you mark a state update as non-urgent. React will process urgent updates first — keeping the input responsive — and handle the transition update when it has time.
Two state values, two update priorities. query updates immediately — the input is always responsive. deferredQuery updates in a transition — the list re-renders when React has a gap between higher-priority work.
isPending is the bonus: a boolean that's true while the transition is in progress. Use it to show a subtle loading indicator without blocking the UI.
useDeferredValue: wrap the value
useDeferredValue works at the value level rather than the update level. You pass it a value and it returns a deferred copy — one that lags behind the real value during expensive renders.
One state value. query updates synchronously on every keystroke — the input is responsive. deferredQuery is a copy of query that React updates at lower priority. The list renders with the deferred value, so expensive list renders don't block the input.
No startTransition. No separate state. Just wrap the value and let React handle the prioritisation.
The real difference
Both approaches solve the same problem. The difference is where you apply the solution.
Use useTransition when you control the state setter. You're the one calling setState — you can wrap that call in startTransition. This is the right tool when the state update and the expensive render are in the same component or when you want the isPending flag.
Use useDeferredValue when you don't control the state setter. The state lives elsewhere — in a parent, in a library, in a URL parameter. You receive a value as a prop and need to defer the expensive work that depends on it. You can't wrap someone else's setState in a transition, but you can defer the value you receive.
In the second example, ResultsList has no control over when query changes — it just receives it. useDeferredValue lets it opt into lower-priority rendering for the expensive computation.
Building something real: a live search with both hooks
Here's a complete live search component that demonstrates both hooks working together — useTransition in the parent for the isPending indicator, useDeferredValue in the results component for the expensive filtering.
Three things working together here:
useTransition in LiveSearch — the query state update is wrapped in startTransition. The input is uncontrolled (onChange without value) so it updates natively at full speed. React processes the state update at lower priority. isPending shows a spinner while the transition is in progress.
useDeferredValue in ResultsList — receives query as a prop and defers it. The filtering runs against deferredQuery, not the latest query. While the deferred value is catching up, isStale is true and the list fades to 60% opacity — a visual cue that results are updating.
React.memo on ResultsList — without this, ResultsList re-renders every time the parent renders, regardless of whether query changed. memo ensures it only re-renders when its props change, which makes the useDeferredValue deferral actually meaningful.
What these hooks are not
They are not debounce. Debounce delays an update by a fixed time. Transitions interrupt when urgent work arrives. They feel similar but behave differently — a 300ms debounce always waits 300ms. A transition waits as long as React needs to, then catches up.
They are not a solution to slow network requests. Transitions affect render priority, not network timing. A slow API call is still slow. Use TanStack Query's keepPreviousData option for the pattern of showing stale results while a new fetch completes.
They are not always necessary. If your expensive render takes under 50ms, users won't notice the difference. Profile before optimising. The React DevTools Profiler shows render durations per component — use it to confirm you have a real problem before adding concurrent hooks.
The decision rule
When in doubt, reach for useDeferredValue first. It's less code and handles the common case. Add useTransition when you need isPending or when you're coordinating multiple state updates that should all transition together.
Up next: The complete guide to React error boundaries in 2026 — what they catch, what they miss, and the pattern that makes them actually useful in production.
Related: Why React.memo doesn't always help — and when it does — the companion article on memoization that pairs naturally with transitions.
Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.
Photo by Victoria Paar on Unsplash

