The useTransition() hook, introduced in React 18, lets you mark certain state updates as non-urgent. This enables React’s concurrent rendering engine to keep the UI responsive by prioritizing urgent updates (like typing) over slower, background ones (like filtering large lists).
In simple terms — it helps React know which updates can wait a little.
// Basic usage of useTransition
const [isPending, startTransition] = useTransition();
isPending → Boolean indicating if a transition is still running.startTransition(callback) → Wrap non-urgent updates inside this function.In this example, the heavy computation blocks the UI, making typing feel sluggish.
import React, { useState } from "react";
function SearchBad() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
// Simulate heavy computation that freezes UI
const filtered = Array(5000)
.fill("Item")
.map((x, i) => x + i)
.filter((x) => x.includes(value));
setResults(filtered);
};
return (
<div className="p-3">
<input value={query} onChange={handleChange} />
<p>Results: {results.length}</p>
</div>
);
}
The input stays responsive because React defers the expensive filtering logic.
import React, { useState, useTransition } from "react";
function SearchGood() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // urgent update (Input reflects immediately)
startTransition(() => {
// non-urgent update (Filtering happens in background)
const filtered = Array(5000)
.fill("Item")
.map((x, i) => x + i)
.filter((x) => x.includes(value));
setResults(filtered);
});
};
return (
<div className="p-3">
<input value={query} onChange={handleChange} />
{isPending ? <p>Filtering…</p> : <p>Results: {results.length}</p>}
</div>
);
}
Since we can't run full React in this view, the demos below use Vanilla JS to simulate the difference in "feel". Try typing quickly in both inputs.
Typing triggers a heavy loop immediately. The browser freezes while calculating.
Input updates immediately. The heavy work is deferred (like startTransition).
⏳ isPending: true...startTransition() are treated as non-urgent.isPending flag lets you show a loading indicator or skeleton UI while the transition processes.| Aspect | Normal Update | Transition Update |
|---|---|---|
| Priority | High (urgent) | Low (non-urgent) |
| Effect on UI | Can cause lag/freeze | Keeps UI responsive |
| Interruptible | No | Yes |
| Ideal For | Typing, clicks, inputs | Filtering, sorting, heavy renders |
startTransition() to avoid UI lag.isPending for lightweight “loading” indicators (like a spinner or dimmed text).useDeferredValue() when you don't have control over the state setter (e.g., props).useTransition() to feel the difference.isPending boolean.Suspense for async data fetching scenarios.