React - Async & Non‑blocking UI
Overview
Estimated time: 20–30 minutes
Learn to keep inputs responsive while rendering expensive updates by deferring non-urgent state transitions.
Try it: Heavy List vs startTransition
View source
const { useMemo, useState, startTransition } = React;
function Heavy({ n }){
// Simulate heavy work
const items = useMemo(() => {
const arr = [];
for (let i=0;i<3000;i++) arr.push(`${n}-${i}`);
return arr;
}, [n]);
return <ul>{items.map((x,i) => <li key={i}>{x}</li>)}</ul>;
}
function App(){
const [text, setText] = useState('');
const [deferred, setDeferred] = useState('');
function onType(e){
const v = e.target.value;
setText(v); // urgent
startTransition(() => setDeferred(v)); // non-urgent
}
return (
<div>
<input value={text} onChange={onType} placeholder="Type to test responsiveness" />
<p>Immediate text: {text}</p>
<Heavy n={deferred} />
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-async')).render(<App />);
Syntax primer
startTransition(() => setState(...))
defers non-urgent updates to keep the UI responsive.- Use memoization to avoid recomputing heavy values on every render.
Common pitfalls
- Putting everything in a transition—only defer non-urgent work.
Checks for Understanding
- What type of work should go inside
startTransition
? - Why might memoization be necessary in heavy render paths?
Show answers
- Non-urgent updates that can be deferred to keep interactions (typing, clicks) responsive.
- To avoid recomputing expensive values on every render and reduce blocking time.
Exercises
- Add a spinner that shows while the deferred work is pending.
- Split the heavy list into virtualized chunks and compare responsiveness.
- Benchmark typing latency with and without
startTransition
.