useTransition: Keep the App Responsive During Expensive Updates
Mark updates as non‑urgent so React can keep typing and clicks responsive.
I had a button that kicks off a second of processing, and while it ran the whole app felt dead. The expensive work and my typing sat at the same priority, so React had no way to put the typing first. useTransition marks the slow update as interruptible, and it gives me isPending so I can show something in the meantime.
Why typing freezes
handleProcess awaits the work and calls setResult like any other update. Nothing marks it as lower priority, so urgent updates like typing and clicks wait their turn:
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;
Two startTransition calls
The fixed handler wraps the work in startTransition, and the result update gets a second startTransition inside it:
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;
While the transition runs, isPending is true and the Loading line shows. The input’s own setInput stays outside any transition, so typing keeps full priority. That’s the whole pattern: urgent updates outside, anything that can wait inside.
What stays urgent
Only non-urgent updates go in a transition; whatever must feel instant stays out. Keep the pending UI light, since a heavy spinner blocks rendering on its own. And when the slow part is really a derived value being passed down the tree, useDeferredValue is the better pairing.