When building React apps, there’s always that awkward moment: the UI lags, a button feels sticky, or typing into a search box suddenly feels like you’re writing in molasses. You check your code; there are no infinite loops and no heavy computations on every keystroke, but still, things don’t feel smooth.
This is where useTransition steps in.
It’s one of those React features that doesn’t try to solve everything. Instead, it solves a particular problem: how to keep your UI responsive while React is busy re-rendering something heavier in the background.
The Problem: When UI Feels Stuck
Imagine a search component.
A user types into an input.
You filter a huge dataset on every keystroke.
Typing slows down because React is busy recalculating and re-rendering.
This happens because React treats all updates as urgent by default. Input keystrokes and expensive state updates both get the same priority. That’s why your UI sometimes feels like it’s “locked up.”
The Old Hacks We Used
Before useTransition, developers reached for tricks like.
Debouncing input: Only trigger updates after the user stops typing. Works, but feels laggy.
Throttling: Similar story.
useEffect with async logic: Push work off, but it’s messy and not guaranteed to feel smooth.
These hacks work around the problem, but they don’t solve it.
Using the useTransition
React 18 gave us useTransition, a hook designed to split updates into two categories.
Urgent updates: Things that must feel instant (like typing, clicking).
Non-urgent updates: Things that can be delayed slightly (like filtering, rendering big lists).
With useTransition, you tell React.
“Hey, this state change is important, but don’t block the urgent stuff. Do it when you can.”
A Simple Example
Here’s how you’d use it in practice.
import { useState, useTransition } from "react";
function Search({ items }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState(items);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
const filtered = items.filter((item) =>
item.toLowerCase().includes(value.toLowerCase())
);
setResults(filtered);
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <p>Updating results…</p>}
<ul>
{results.map((r) => (
<li key={r}>{r}</li>
))}
</ul>
</div>
);
}

Join the conversation! Your thoughts help the community grow.