If you have ever typed into a React search box and watched the UI hitch while a long list re-renders, you have already felt the problem startTransition was built to solve. It does not make expensive work disappear. It tells React that some updates are less urgent, so interactive work like typing, clicking, and focusing can stay responsive.
That distinction matters more in modern React than it used to. When a change triggers heavy rendering, the browser can feel stuck even if the code is technically correct. startTransition gives you a way to separate the update the user is actively driving from the work that can wait a moment.
What startTransition actually does
1 Marks updates as non-urgent
React can delay or interrupt the work if something more important happens first.
2 Protects the urgent path
Input state, clicks, and other immediate interactions keep a higher priority.
3 Does not add background threads
It changes scheduling priority inside React, not how the browser executes JavaScript.
A simple example
The most direct use case is a UI update that can wait while something else stays responsive, such as switching between panels or filters.
import { startTransition, useState } from 'react';
function TabSwitcher() {
const [tab, setTab] = useState('overview');
function handleChange(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<button onClick={() => handleChange('overview')}>Overview</button>
<button onClick={() => handleChange('details')}>Details</button>
<button onClick={() => handleChange('reviews')}>Reviews</button>
<section>{tab}</section>
</div>
);
}
The important part is not the button click itself. The key is that the state update inside startTransition can be interrupted if a more urgent update arrives.
Why it helps with slow interactions
The most common use case is input that drives expensive derived rendering. Think search filters, large tables, or complex dashboards.
When the user types, the input should update immediately. The expensive list filtering, layout recalculation, or chart refresh can happen as a lower-priority update.
import { startTransition, useMemo, useState } from 'react';
const people = [
'Ada Lovelace',
'Grace Hopper',
'Edsger Dijkstra',
'Linus Torvalds',
'Margaret Hamilton',
];
export default function SearchPeople() {
const [input, setInput] = useState('');
const [query, setQuery] = useState('');
const filteredPeople = useMemo(() => {
return people.filter((person) =>
person.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
function handleChange(event) {
const nextValue = event.target.value;
setInput(nextValue);
startTransition(() => {
setQuery(nextValue);
});
}
return (
<div>
<input value={input} onChange={handleChange} placeholder="Search people" />
<ul>
{filteredPeople.map((person) => (
<li key={person}>{person}</li>
))}
</ul>
</div>
);
}
This pattern keeps the text field snappy even if the filtered list is expensive to render. The input state stays urgent. The query state becomes a transition.
Urgent vs transition updates
| Update type | Example | How React treats it |
|---|---|---|
| Urgent | Typing into an input | React should keep the UI responsive immediately |
| Transition | Filtering a large result list | React may delay or interrupt the render if needed |
| Urgent | Clicking a menu item | The interface should respond without waiting on heavier work |
| Transition | Rendering a new dashboard view | The new screen can be prepared without blocking the interaction that triggered it |
The mental model is simple: urgent work keeps the app feeling alive, while transition work is useful but not time-critical.
Using useTransition with pending state
If you want the UI to show that a transition is still in progress, pair startTransition with useTransition.
import { useState, useTransition } from 'react';
export default function ProductTabs({ products }) {
const [tab, setTab] = useState('all');
const [isPending, startTransition] = useTransition();
function handleTabChange(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
const visibleProducts = products.filter((product) =>
tab === 'all' ? true : product.category === tab
);
return (
<div>
<button onClick={() => handleTabChange('all')}>All</button>
<button onClick={() => handleTabChange('books')}>Books</button>
<button onClick={() => handleTabChange('tools')}>Tools</button>
{isPending ? <p>Updating list...</p> : null}
<ul>
{visibleProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
What startTransition is not
1 Not a performance optimizer by itself
It changes priority, but it does not make an expensive render cheaper.
2 Not for every state update
Use it when the update can wait. Keep urgent UI state outside the transition.
3 Not a network loading API
It does not fetch data or cancel requests. It only marks React work as lower priority.
Practical rules of thumb
Bottom line
startTransition is React's way of saying, "this update matters, but it does not need to interrupt the user." That makes it a good fit for expensive views driven by typing, tab changes, filters, and other interactions where responsiveness matters more than finishing everything immediately.
Use it to protect the urgent path. Keep the immediate UI fast, and let the heavier render catch up in the background.