close

DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

React 19 useTransition & useDeferredValue: When to Use Which (2026)

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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 — useDeferredValue does 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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Full article at stacknotice.com/blog/react-19-transitions-guide-2026

Top comments (0)