Keep urgent inputs responsive while less urgent UI catches up.
Read the explanation, run the worked code, then complete the practice lab in Assignments. This advanced lesson includes worked code and a dedicated practice lab.
Understand the idea
Not every update has the same urgency. A controlled text input should reflect typing promptly; a large result panel can follow as a lower-priority update. useDeferredValue lets a consumer temporarily use an older value while React prepares the new UI. useTransition marks state updates as non-urgent and exposes pending state. Neither runs JavaScript on a worker or makes a single expensive calculation interruptible halfway through.
The example defers a search query sent to a memoized result component. The input follows query immediately, while the result region may briefly show the previous query. Mark that region as stale instead of pretending it is current. Fast machines or small datasets may show no perceptible lag. Reduce the amount of work as well: for very large lists, pagination or virtualization can be more useful than rendering thousands of nodes.
For a tab switch controlled by a state setter, useTransition can be appropriate. Do not put the state update controlling a text input inside a transition. Deferred values do not debounce requests: a request may still begin for every keystroke. Network cancellation, caching and rate control remain separate concerns.
Run the example
Use a separate React practice project with a Node.js version supported by Vite. Create it once with these commands, then use its development URL. Do not paste this into the Edupoly website’s own source files.
npm create vite@latest react-practice -- --template react
cd react-practice
npm install
npm run dev
Replace src/App.jsx with the following complete example. Keep Vite’s generated src/main.jsx entry point.
src/App.jsx
import { memo, useDeferredValue, useState } from 'react';
const records = Array.from({length: 3000}, (_, id) => ({id, name: `Student ${id}`}));
const Results = memo(function Results({query}) {
return <ul>{records.filter(row => row.name.toLowerCase().includes(query.toLowerCase())).map(row => <li key={row.id}>{row.name}</li>)}</ul>;
});
export default function App() {
const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query);
const stale = query !== deferredQuery;
return <main><label>Search students <input value={query} onChange={e => setQuery(e.target.value)} /></label>
<p role="status">{stale ? 'Updating results…' : 'Results are current'}</p>
<section aria-busy={stale} style={{opacity: stale ? 0.6 : 1}}><Results query={deferredQuery} /></section>
</main>;
}
Expected result
Typing updates the input immediately. Results eventually match the latest query; a busy label may briefly appear during slower rendering.
Apply this to your assignments
Compare the example with deferredQuery replaced by query under CPU throttling. Then create a tabbed report and use useTransition for changing the selected tab. Keep the search input’s state urgent. Explain why deferral does not reduce the number of API requests by itself.
Practice starting point
Use the worked example above as your starting point.
Open Assignments for this lesson’s practice lab and acceptance checks.
Common mistakes
Do not add artificial blocking loops to production. A transition changes scheduling, not the cost of your computation. Always make stale results visually understandable.
Check your understanding
- Predict the output before running the example, then compare it with the result panel.
- Explain which value each expression reads and which action changes it.
- Try the boundary cases listed in the practice task and record what you observe.
- Rebuild one matching assignment without copying, then compare behavior with the original example.
Reference
Official documentation for this topic · Vite setup requirements
