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

  1. What type of work should go inside startTransition?
  2. Why might memoization be necessary in heavy render paths?
Show answers
  1. Non-urgent updates that can be deferred to keep interactions (typing, clicks) responsive.
  2. To avoid recomputing expensive values on every render and reduce blocking time.

Exercises

  1. Add a spinner that shows while the deferred work is pending.
  2. Split the heavy list into virtualized chunks and compare responsiveness.
  3. Benchmark typing latency with and without startTransition.