Suggest an editImprove this articleRefine the answer for “useTransition and useDeferredValue in React”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**useTransition** and **useDeferredValue** are React 18 hooks for concurrent rendering. `useTransition` wraps a state setter and returns `[isPending, startTransition]`, giving you a loading indicator. `useDeferredValue` wraps a value, useful when you only receive a prop from a parent. ```tsx // You own the setter const [isPending, startTransition] = useTransition(); startTransition(() => setResults(filter(query))); // You only have the value const deferred = useDeferredValue(query); ``` **Rule:** control the setter - `useTransition`; receive a prop - `useDeferredValue`.Shown above the full answer for quick recall.Answer (EN)Image**useTransition** marks state updates as low-priority work React can interrupt; **useDeferredValue** defers a value's re-render until urgent updates commit first. ## Theory ### TL;DR - Analogy: `useTransition` is like telling React "filter this list in the background while I keep typing" - input stays instant, results catch up after. - `useTransition` wraps state setters and returns `[isPending, startTransition]`; `useDeferredValue` wraps a value and returns a deferred copy. - Own the state setter: use `useTransition`. Receive a value as a prop: use `useDeferredValue`. - Need a loading indicator: only `useTransition` has `isPending`. `useDeferredValue` does not. - Both require React 18 with concurrent rendering enabled. ### Quick example ```tsx import { useState, useTransition } from 'react'; function Search() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { setQuery(e.target.value); // Urgent: commits before next paint startTransition(() => { setResults(heavySearch(e.target.value)); // Low-priority: yields to input }); }; return ( <div> <input value={query} onChange={handleChange} /> {isPending && <p>Searching...</p>} <ul>{results.map(r => <li key={r}>{r}</li>)}</ul> </div> ); } ``` Type fast and the input updates on every keystroke. `isPending` is `true` while the background filter runs. Results appear once typing slows. ### Key difference `useTransition` requires access to the state setter. You call `startTransition(() => setState(...))` and get `isPending` back. `useDeferredValue` only needs the value itself, making it the right pick when a child component receives a prop from a parent it does not own. No `isPending` signal, but you can detect staleness by comparing the original value to the deferred copy (`query !== deferredQuery`). ### When to use - Typing in a search field with heavy client-side filtering: `useTransition` (wrap `setResults`). - A child component renders a large list from a prop: `useDeferredValue` (defer the list re-render). - Tab switch that triggers a heavy render: `useTransition` (non-urgent tab change with `isPending` for disabled buttons). - Debounced display without debouncing the input: `useDeferredValue` (input commits instantly, results lag). - You need a spinner: `useTransition`. No spinner needed: either works, but `useDeferredValue` is simpler. ### Comparison table | | **useTransition** | **useDeferredValue** | |---|---|---| | What it defers | A state update | A value | | Where to apply | You control the setter | You only have the value | | Returns | `[isPending, startTransition]` | Deferred copy of the value | | Pending indicator | Built-in `isPending` | Compare original vs deferred manually | | Typical location | Event handler | Component body or child component | ### How React schedules this internally React 18 assigns every update a "lane" inside the fiber reconciler. Urgent updates (input events) get `SyncLane` or `DefaultLane`. Anything wrapped in `startTransition` gets `TransitionLane`, which has lower priority. During the render loop, the scheduler runs `shouldYield()` after each unit of work. If a higher-priority lane has pending work, the low-priority render pauses and resumes later. `useDeferredValue` uses the same lane system without touching the setter. React renders the component twice: once with the current value (urgent path), once with the deferred value (low-priority path). The deferred render can be interrupted at any point. This is React's own fiber scheduler, not `requestIdleCallback`. ### Common mistakes **1. Treating `isPending` as an empty-state check** ```tsx // Wrong: spins even when results are genuinely empty {results.length === 0 && <Spinner />} // Right: spins only during an active transition {isPending && <Spinner />} ``` **2. Wrapping the input setter inside `startTransition`** The most common mistake I catch in code review: putting the input state update inside the transition. ```tsx // Wrong: input lags, defeats the purpose startTransition(() => { setQuery(e.target.value); }); // Right: input outside, heavy work inside setQuery(e.target.value); startTransition(() => setResults(heavySearch(e.target.value))); ``` **3. Assuming `useDeferredValue` cancels expensive work** ```tsx // Wrong assumption const deferred = useDeferredValue(expensiveCompute(query)); // expensiveCompute still runs on every render regardless ``` Deferring only changes when React commits the render. `expensiveCompute(query)` still runs. Wrap the expensive call in `useMemo` keyed to the deferred value, or handle async work with an abort controller. **4. Nesting `startTransition` calls** ```tsx // Wrong: confusing priority startTransition(() => { startTransition(() => setDeepState(val)); }); ``` Use one outer `startTransition`. Nesting does not increase priority and can cause unpredictable pending states. **5. Ignoring StrictMode double-invoke behavior** In development with StrictMode, React 18 double-invokes state updaters inside transitions. `isPending` may flicker. This is expected in dev mode and does not appear in production builds. ### Real-world usage - React DevTools: `useTransition` for filter toggles in the component tree panel. - TanStack Query v5: wraps list refetch after mutations in transitions. - Next.js App Router: `useDeferredValue` defers search results in dynamic route segments. - Vercel Commerce template: search debouncing via deferred query against a large product catalog. ### Follow-up questions **Q:** What is the difference between `useTransition` and the standalone `startTransition` imported from `react`? **A:** Standalone `startTransition` does the same job but returns nothing. Use it in utility functions or event handlers outside components where you do not need `isPending`. **Q:** How does `useDeferredValue` interact with `Suspense`? **A:** If the deferred render reads from a suspended resource, React shows the stale UI instead of the `Suspense` fallback until the deferred render finishes. This avoids a flash-to-spinner on every keystroke. **Q:** Why might `isPending` stay `true` for a long time? **A:** A new transition starting before the previous one resolves keeps the flag `true`. An unhandled error inside the transition callback also blocks the queue. **Q:** What lane priorities does the React scheduler use internally? **A:** Urgent input: `SyncLane` or `DefaultLane`. `startTransition` work: `TransitionLane`. Background tasks: `IdleLane`. The reconciler yields whenever `shouldYield()` returns `true`, which happens when a higher-priority lane has pending work. **Q:** How would you implement transition preemption in a custom scheduler? Walk through fiber lane assignment. **A:** Assign a low-priority lane to the transitioning fiber during `scheduleUpdateOnFiber`. In the work loop, check `shouldYield()` after each fiber unit. If a higher lane has work, break and post a new task for that lane. The in-progress low-priority work stays in the tree and resumes after the high-priority pass completes. React does not discard partial work, it suspends it. ## Examples ### Basic: filtering a large list with useTransition ```tsx import { useState, useTransition } from 'react'; const items = Array.from({ length: 10000 }, (_, i) => `Product ${i + 1}`); function ProductList() { const [query, setQuery] = useState(''); const [results, setResults] = useState(items); const [isPending, startTransition] = useTransition(); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value; setQuery(value); // Commits immediately, before filter runs startTransition(() => { setResults( items.filter(item => item.toLowerCase().includes(value.toLowerCase())) ); }); }; return ( <div> <input value={query} onChange={handleChange} placeholder="Search..." /> {isPending && <p>Filtering...</p>} <ul> {results.map(item => <li key={item}>{item}</li>)} </ul> </div> ); } ``` The input field commits on every keystroke without waiting for the 10k-item filter. `isPending` shows a message while the background render works. No debounce needed. ### Intermediate: useDeferredValue in a child component This pattern appears in e-commerce templates where a child component only receives `query` as a prop: ```tsx import { useState, useDeferredValue, useMemo } from 'react'; interface Product { id: number; name: string; } function ProductResults({ query, products }: { query: string; products: Product[] }) { const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; // True while deferred lags behind const filtered = useMemo( () => products.filter(p => p.name.toLowerCase().includes(deferredQuery.toLowerCase()) ), [products, deferredQuery] // Only re-runs when deferredQuery changes ); return ( <ul style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 0.15s' }}> {filtered.map(p => <li key={p.id}>{p.name}</li>)} </ul> ); } function App({ products }: { products: Product[] }) { const [query, setQuery] = useState(''); return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} /> <ProductResults query={query} products={products} /> </div> ); } ``` `ProductResults` does not own the setter, so `useTransition` is not an option here. The opacity fade signals to the user that a fresher result is on the way. ### Advanced: combining both hooks in one component In production you often need both at once. `useTransition` for a tab switch (you control `setTab`), `useDeferredValue` for the query filter (you only need the value): ```tsx import { useState, useTransition, useDeferredValue, useMemo } from 'react'; type Tab = 'active' | 'archived'; interface Item { id: number; name: string; status: Tab; } function AdminDashboard({ items }: { items: Item[] }) { const [query, setQuery] = useState(''); const [tab, setTab] = useState<Tab>('active'); const [isPending, startTransition] = useTransition(); const deferredQuery = useDeferredValue(query); const handleTabChange = (next: Tab) => { startTransition(() => setTab(next)); // Non-urgent, but you own the setter }; const visible = useMemo( () => items .filter(item => item.status === tab) .filter(item => item.name.toLowerCase().includes(deferredQuery.toLowerCase()) ), [items, tab, deferredQuery] ); return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Filter..." /> <button onClick={() => handleTabChange('active')} disabled={isPending}> Active {isPending && '...'} </button> <button onClick={() => handleTabChange('archived')} disabled={isPending}> Archived {isPending && '...'} </button> <ul style={{ opacity: query !== deferredQuery ? 0.7 : 1 }}> {visible.map(item => <li key={item.id}>{item.name}</li>)} </ul> </div> ); } ``` Tab buttons disable during the transition (`isPending`). The list uses `deferredQuery` so typing does not block the tab switch animation. Both hooks in the same component, each doing exactly the job it is designed for.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.