useTransition: Keep the App Responsive During Expensive Updates
Mark updates as non‑urgent so React can keep typing and clicks responsive.
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
-
Call
const [isPending, startTransition] = useTransition(). -
In the handler, wrap the async work with
startTransition. -
After it resolves, wrap the state update in another
startTransition. -
Show pending UI when
isPendingis true. -
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
useDeferredValuewhen 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));
});