Both useTransition and useDeferredValue exist because React 19 cares about which renders are urgent and which can wait. But they operate at different points in the update cycle.
Here's the clearest way to think about it:
-
useTransition— you control when the state update happens. You wrap the setter. -
useDeferredValue— you control when the new value propagates. You wrap the value.
Same goal, different leverage point.
What "Transition" Actually Means
React's concurrent mode introduced the concept of priority. Not every update needs to happen at the same urgency. Typing a keystroke should respond immediately. Rendering 500 filtered items can wait.
A "transition" marks an update as low-priority — one that can be deferred if something urgent arrives.
useTransition: Wrap the Setter
import { useState, useTransition } from 'react'
function FilteredList({ items }: { items: string[] }) {
const [query, setQuery] = useState('')
const [filteredItems, setFilteredItems] = useState(items)
const [isPending, startTransition] = useTransition()
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value
setQuery(value) // urgent — input updates immediately
startTransition(() => {
// non-urgent — filtering can be deferred
setFilteredItems(items.filter((item) =>
item.toLowerCase().includes(value.toLowerCase())
))
})
}
return (
<div>
<input value={query} onChange={handleSearch} placeholder="Filter..." />
{isPending && <span className="text-sm text-muted-foreground">Updating...</span>}
<ul className={isPending ? 'opacity-50' : ''}>
{filteredItems.map((item) => <li key={item}>{item}</li>)}
</ul>
</div>
)
}
isPending is true while React is computing the transition. Use it to show a spinner or dim stale content.
useTransition with Server Actions
In React 19, startTransition accepts async callbacks — making it the right way to track Server Action state:
'use client'
import { useTransition } from 'react'
import { updateProfile } from './actions'
export function ProfileForm({ user }: { user: { name: string; bio: string } }) {
const [isPending, startTransition] = useTransition()
function handleSubmit(formData: FormData) {
startTransition(async () => {
await updateProfile(formData) // Server Action
})
}
return (
<form action={handleSubmit}>
<input name="name" defaultValue={user.name} />
<textarea name="bio" defaultValue={user.bio} />
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save changes'}
</button>
</form>
)
}
No manual useState boolean for loading state — isPending handles it.
Navigation Loading State
'use client'
import { useRouter } from 'next/navigation'
import { useTransition } from 'react'
export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
const router = useRouter()
const [isPending, startTransition] = useTransition()
return (
<button
onClick={() => startTransition(() => router.push(href))}
className="relative"
>
{children}
{isPending && <span className="absolute inset-x-0 -bottom-0.5 h-0.5 bg-primary animate-pulse" />}
</button>
)
}
useDeferredValue: Wrap the Value
Use this when you receive a value from outside and passing it to an expensive component causes slowdown.
import { useState, useDeferredValue, memo } from 'react'
// CRITICAL: memo() is required for useDeferredValue to help
const MemoizedSearchResults = memo(SearchResults)
export function SearchPage() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<div className={isStale ? 'opacity-60' : ''}>
<MemoizedSearchResults query={deferredQuery} />
</div>
</div>
)
}
The input always shows the latest value. The expensive search results catch up when React has a free frame.
memo()is required. Without it, the component re-renders on every keystroke regardless of the deferred value —useDeferredValuedoes nothing.
How it works frame by frame
When the user types "react" fast:
Keystroke "r": query = "r", deferredQuery = "" → shows "" results
Keystroke "e": query = "re", deferredQuery = "r" → shows "r" results
...pause...
After pause: query = "react", deferredQuery = "react" → shows "react" results
The input is always instant. Results catch up.
useOptimistic: The Third Member
React 19's useOptimistic pairs with useTransition for instant optimistic UI:
'use client'
import { useOptimistic, useTransition } from 'react'
import { toggleLike } from './actions'
export function LikeButton({ postId, initialLikes, initialLiked }) {
const [isPending, startTransition] = useTransition()
const [optimistic, addOptimistic] = useOptimistic(
{ likes: initialLikes, liked: initialLiked },
(state, liked: boolean) => ({
likes: liked ? state.likes + 1 : state.likes - 1,
liked,
})
)
function handleClick() {
const newLiked = !optimistic.liked
startTransition(async () => {
addOptimistic(newLiked) // instant UI update
await toggleLike(postId, newLiked) // server mutation
})
}
return (
<button onClick={handleClick} disabled={isPending}>
{optimistic.liked ? '❤️' : '🤍'} {optimistic.likes}
</button>
)
}
If the server call fails, React rolls back automatically.
The Actual Difference
// useTransition: wrap the setter
const [isPending, startTransition] = useTransition()
startTransition(() => setState(newValue))
// useDeferredValue: wrap the value
const deferredValue = useDeferredValue(value)
<ExpensiveComponent data={deferredValue} />
useTransition |
useDeferredValue |
|
|---|---|---|
| What you wrap | The state setter | The value itself |
| isPending | Yes | No (compute manually) |
| Requires memo() | No | Yes |
| Best for | Server Actions, navigation | Search, prop-driven renders |
Common Mistakes
1. Using useTransition for animations — it's about rendering priority, not CSS animations.
2. Forgetting memo() with useDeferredValue — without it, nothing is deferred.
3. Marking urgent updates as transitions:
// Wrong — input updates should always be urgent
startTransition(() => setInputValue(e.target.value))
// Right
setInputValue(e.target.value) // urgent
startTransition(() => setFilteredResults(filter(...))) // non-urgent
Full article at stacknotice.com/blog/react-19-transitions-guide-2026
Top comments (0)