Back to all notes

useTransition: Keep the App Responsive During Expensive Updates

Mark updates as non‑urgent so React can keep typing and clicks responsive.

2 min read

Introduction

useTransition lets you mark certain state updates as “transitions”—work that can be interrupted in favor of more urgent updates like typing and clicks. It’s React’s way of helping you keep the UI responsive during heavy renders.

Why this matters

Some updates are expensive—filtering large datasets, rendering big lists. useTransition lets you mark those updates as lower priority and provides isPending to show feedback.

The problem

Awaiting expensive work on the same priority as input updates can freeze typing and clicks.

Inefficient approach

All work happens at the same priority, blocking urgent updates:

import { useState } from "react";

async function asyncFunction(input) {
  await new Promise(resolve => setTimeout(resolve, 1000));
  return `Processed: ${input}`;
}

function App() {
  const [input, setInput] = useState("");
  const [result, setResult] = useState("");

  const handleProcess = async () => {
    const processed = await asyncFunction(input);
    setResult(processed);
  };

  return (
    <div>
      <h1>useTransition</h1>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Enter text..."
      />
      <button onClick={handleProcess}>Process</button>
      {result && <p>{result}</p>}
    </div>
  );
}

export default App;

The solution

Wrap the slow work and the resulting state update in startTransition.

import { useState, useTransition } from "react";

async function asyncFunction(input) {
  await new Promise(resolve => setTimeout(resolve, 1000));
  return `Processed: ${input}`;
}

function App() {
  const [input, setInput] = useState("");
  const [result, setResult] = useState("");
  const [isPending, startTransition] = useTransition();

  const handleProcess = async () => {
    startTransition(async () => {
      const processedResult = await asyncFunction(input);
      startTransition(() => setResult(processedResult));
    });
  };

  return (
    <div>
      <h1>useTransition</h1>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Enter text..."
      />
      <button onClick={handleProcess}>Process</button>
      {isPending && <p>Loading...</p>}
      {result && <p>{result}</p>}
    </div>
  );
}

export default App;

Step-by-step

  1. Call const [isPending, startTransition] = useTransition().

  2. In the handler, wrap the async work with startTransition.

  3. After it resolves, wrap the state update in another startTransition.

  4. Show pending UI when isPending is true.

  5. Keep input updates outside transitions so they stay urgent.

Tips

  • Only wrap non‑urgent updates. Keep urgent updates (like input value) outside.

  • Show lightweight pending UI; avoid heavy spinners that block rendering.

  • Pair with useDeferredValue when passing slow, derived values down the tree.

Knowledge base

Problem snippet:

const processed = await asyncFunction(input);
setResult(processed);

Solution snippet:

startTransition(async () => {
  const processedResult = await asyncFunction(input);
  startTransition(() => setResult(processedResult));
});