Ever had a React app where an update causes a janky UI or a spinner that pops up out of nowhere , but only sometimes? I ran into this recently when trying to make a search input feel smooth while fetching results. I used useTransition to defer the heavy update, but I didn’t fully grasp what React was doing behind the scenes.
Turns out, useTransition is not just a fancy hook to slap on some state. It’s a clever way React’s concurrent rendering lets you mark updates as "non-urgent." That impacts scheduling, priority, and how your UI feels to users.
Let me walk you through what really happens under the hood with useTransition, how it interacts with React’s scheduler, and when it actually helps your app’s performance.
The developer moment: Why did my spinner randomly appear?
I had a component with a search box. Typing updated a local state immediately (the input’s value), but then I also kicked off a data fetch and updated the results state. The results update triggered a big list render, which sometimes made typing laggy.
I wrapped the results state update in a startTransition callback from useTransition:
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function onChange(e) {
setQuery(e.target.value); // urgent update, keep input responsive
startTransition(() => {
fetchResults(e.target.value).then(setResults); // deferred update
});
}
Now React shows a spinner when isPending is true, which means the transition is in flight. But the spinner sometimes flashes for just a split second or not at all. Why?
The answer lies in how React prioritizes these updates and schedules rendering work.
useTransition under the hood: marking updates as low priority
React’s concurrent mode lets your app interrupt rendering work to keep the UI responsive. Priorities matter. User input is high priority. Visual updates that could wait? Lower priority.
useTransition leverages this by tagging state updates inside startTransition as "transitions": low priority work.
When you call startTransition(() => setState(newValue)), React:
- Marks that update as non-urgent.
- Schedules it with a lower priority than input or animation updates.
- Allows React to keep rendering high priority work first (like updating the input value).
This means your input feels snappy even if the results list takes time to render or fetch.
What happens in React’s scheduler?
React’s scheduler manages a queue of updates with priorities:
- High priority: user input, clicks, focus
- Transition priority: updates inside
startTransition
- Normal priority: other updates
Imagine React like a cook in a busy kitchen. Urgent orders (user input) get served immediately. Less urgent ones (transitions) wait for a free moment.
When a transition update is scheduled, React tries to render it in chunks, yielding control back to the browser if there’s more urgent work.
If the user keeps typing fast, React can pause rendering the transition and show the latest input first.
This is why your spinner may flash briefly:
- If the transition finishes very fast, React commits it immediately, so
isPending toggles quickly.
- If the user interrupts by typing more, React may drop the previous transition and start a new one, causing flickers.
The isPending flag: what you’re really tracking
useTransition gives you isPending, a boolean that indicates if there’s any transition update still "in progress."
But "in progress" here means "React hasn’t committed the transition update to the DOM yet."
It doesn’t mean your fetch is still pending (you have to track that yourself). It means React’s rendering of the update is ongoing or waiting.
Because React batches and may interrupt work, isPending can toggle quickly, making spinners flash unexpectedly.
When to use useTransition , and when not to
useTransition shines when you want to:
- Keep input or UI responsiveness snappy by deferring heavy updates
- Show some feedback (like a spinner) while transition updates are rendering
- Avoid blocking urgent updates with less urgent ones
But it’s not a silver bullet:
- If your deferred update is tiny or fast, the spinner can feel like flicker. You might prefer to skip it.
- If you don’t handle loading states well, users might get confused.
- If you’re not in concurrent mode or React 18+,
useTransition won’t do much.
A concrete example: search input with and without useTransition
Imagine a big list that takes 300ms to render.
Without useTransition:
- User types a letter
- React updates query and results immediately
- UI blocks for 300ms, input lags or freezes
With useTransition:
- User types a letter
- React updates query immediately (urgent)
- React schedules results update as a transition
- Input stays responsive
- Spinner shows while results render
But if the user types multiple letters quickly, React might skip rendering intermediate results, showing only the latest. The spinner might flash briefly or never appear if rendering is too fast.
Debugging tip: React DevTools Profiler and isPending
React DevTools Profiler can show you when transitions start and end. You’ll see how React schedules and prioritizes updates.
When debugging flickering spinners or janky UI, check:
- Are you using
startTransition correctly?
- Is your deferred update actually expensive?
- How often does
isPending toggle?
You can also throttle your CPU or network to simulate slow rendering and fetches, making the transition more visible.
What happens if you nest transitions or mix priorities?
React lets you nest transitions, but inside a transition, a state update is always low priority unless you explicitly mark it urgent.
Mixing urgent and transition updates can lead to surprising UI behaviors if not managed carefully.
For example, if you update input state inside a transition, input responsiveness suffers. Always update urgent UI state outside startTransition.
Wrapping up
useTransition is a subtle but powerful tool to tell React, "This update can wait a bit." It works by marking state updates as low priority, letting React’s concurrent scheduler keep your UI responsive.
Understanding the scheduler, priorities, and what isPending really means helps you use useTransition effectively and avoid flickery spinners or janky typing.
Next time your React UI feels sluggish or your loading spinners flash unpredictably, remember what’s happening under the hood with transitions , it’s a conversation between your code and React’s scheduler to keep things smooth.
Give it a try in your app, measure with React DevTools, and tweak your priorities to find the sweet spot between responsiveness and smooth updates.