useTransition Hook
Overview
React 18 introduced Concurrent Rendering, an engine that allows React to pause, abort, or prioritize different UI updates. The primary API for interacting with this engine is the `useTransition` hook.
Historically, all state updates in React were considered 'urgent'. If you clicked a button to filter a list of 10,000 items, React would lock up the main thread to process the math. The browser would completely freeze; you couldn't click anywhere else or type in an input until the list finished rendering.
useTransition allows you to mark specific state updates as 'non-urgent' (Transitions).
If you wrap a state update in a Transition, React will attempt to render the heavy UI in the background. Crucially, if the user tries to do something urgent (like typing in a search bar) while the heavy render is happening, React will immediately pause the heavy render, instantly update the search bar, and then resume the heavy render afterwards. This keeps the application perfectly responsive under heavy CPU load.
Syntax
import { useState, useTransition } from 'react';
function HeavyDashboard() {
const [tab, setTab] = useState('home');
// 1. Initialize the hook
// isPending is a boolean that is true while the background render is processing
const [isPending, startTransition] = useTransition();
const switchTab = (newTab) => {
// 2. Wrap the heavy state update in startTransition
// We are telling React: "Updating this tab might take a long time.
// Don't freeze the screen. Keep the old UI interactive while you process this."
startTransition(() => {
setTab(newTab);
});
};
return (
<div>
<button onClick={() => switchTab('home')}>Home</button>
<button onClick={() => switchTab('heavyAnalytics')}>Analytics</button>
{/* 3. We can use isPending to show a loading indicator without destroying the old UI */}
{isPending && <span className="spinner">Loading tab...</span>}
<TabContent activeTab={tab} />
</div>
);
}Common Pitfalls
- Wrapping urgent updates: Do not wrap input typing (
onChange={(e) => setQuery(e.target.value)}) in a transition! If a user is typing on their keyboard, they expect the letters to appear instantaneously. Typing is an urgent update. Only wrap the heavy results of the typing (like filtering the massive array) in a transition.
Interview Questions
A normal state update is urgent and blocking; React will freeze the browser until the render is complete. A Transition is non-urgent and interruptible; React renders it in the background, allowing the browser to remain fully responsive to user interactions like clicking and typing.
useTransition differ from setTimeout or Debouncing?setTimeout simply delays the execution of the function, but when it finally runs, it still blocks the main thread. Debouncing artificially forces the user to wait a fixed amount of time before executing. useTransition actually executes immediately and uses Concurrent Rendering to weave the execution into the browser's idle frames, keeping the thread unblocked.
Real-World Example
Invoking Server Actions without Forms: In the modern Server Components era, useTransition has become the standard mechanism for tracking the pending/loading state of asynchronous Server Actions triggered by UI buttons.
// useTransition is heavily used in Next.js App Router to trigger
// Server Actions programmatically (e.g., clicking a Like button).
import { useTransition } from 'react';
import { likePostAction } from './actions';
export function LikeButton({ postId }) {
const [isPending, startTransition] = useTransition();
const handleLike = () => {
// We wrap the Server Action in a transition.
// 'isPending' will remain true while the network request travels to the
// server, executes the DB mutation, and returns the revalidated HTML.
startTransition(async () => {
await likePostAction(postId);
});
};
return (
<button onClick={handleLike} disabled={isPending}>
{isPending ? 'Saving...' : '❤️ Like'}
</button>
);
}Check Your Knowledge
Test your understanding of useTransition Hook with these quick questions.