Skip to main content

useTransition Hook: React Beginners Guide 2026

useTransition is a React 19 hook that lets you mark a state update as non-urgent, keeping the UI responsive while the update processes in the background. When you call useTransition, it returns a boolean flag isPending (true while the transition is running) and a function startTransition that wraps a state update. React will interrupt that update if the user clicks, types, or interacts with something else, ensuring the UI never feels frozen.

The hook solves a common performance problem: when a state change triggers a slow render (like filtering a large list or searching), the UI blocks until React finishes. useTransition tells React to deprioritize that render so the browser can respond to user input immediately. The result is a responsive, professional-feeling interface.

When to Use useTransition vs. Regular State

Use useTransition when a state update will trigger an expensive render that might block the UI. Common scenarios include:

  • Filtering or searching a large list (>10,000 items)
  • Complex form validation with real-time feedback
  • Sorting large datasets on demand
  • Rendering after a slow API call completes

Do NOT use useTransition for simple state updates (toggles, counters, modals) that render instantly.

Here's the mental model: useTransition says "I know this render is expensive; please don't block the main thread."

Basic useTransition Pattern

import { useState, useTransition } from 'react';

export function SearchUsers() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();

const handleSearch = (e) => {
const newQuery = e.target.value;
// Update input immediately (high priority)
setQuery(newQuery);

// Filter results as a transition (low priority)
startTransition(() => {
const filtered = users.filter(user =>
user.name.toLowerCase().includes(newQuery.toLowerCase())
);
setResults(filtered);
});
};

return (
<div>
<input
type="text"
value={query}
onChange={handleSearch}
placeholder="Search users (type feels instant)"
/>
{isPending ? (
<p>Loading results...</p>
) : (
<ul>
{results.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}

// Mock user data (in a real app, this would be from an API)
const users = Array.from({ length: 100000 }, (_, i) => ({
id: i,
name: `User ${i}`,
}));

In this example:

  1. setQuery(newQuery) updates the input immediately (high priority).
  2. startTransition(() => { setResults(...) }) marks the filter as a transition.
  3. isPending is true while the filter runs, so you can show a loading message.
  4. If the user types again while the filter is running, React cancels the old filter and starts a new one—the input stays responsive.

The isPending Flag: Showing Feedback

isPending is crucial for user experience. While a transition is running, isPending is true. Use it to show visual feedback:

import { useState, useTransition } from 'react';

export function FilterProducts() {
const [category, setCategory] = useState('all');
const [products, setProducts] = useState([]);
const [isPending, startTransition] = useTransition();

const handleCategoryChange = (newCategory) => {
setCategory(newCategory); // Immediate update to UI

startTransition(() => {
// Expensive: filter a large product database
const filtered = filterProductsByCategory(newCategory);
setProducts(filtered);
});
};

return (
<div>
<select value={category} onChange={e => handleCategoryChange(e.target.value)}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>

{/* Show a visual indicator while filtering */}
{isPending && (
<div className="spinner" style={{ opacity: 0.6 }}>
Filtering products...
</div>
)}

<ul>
{products.map(product => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
</div>
);
}

function filterProductsByCategory(category) {
// Simulate expensive filtering
const allProducts = generateProducts(100000);
return allProducts.filter(p =>
category === 'all' || p.category === category
);
}

function generateProducts(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `Product ${i}`,
category: ['electronics', 'clothing', 'food'][i % 3],
price: Math.random() * 500,
}));
}

The spinner only appears because isPending is managed by React's transition. The input updates instantly, but the list update is deprioritized.

Multiple Transitions in One Component

You can use useTransition multiple times if different parts of your component need independent transitions:

import { useState, useTransition } from 'react';

export function Dashboard() {
const [data, setData] = useState(null);
const [pending1, startTransition1] = useTransition();
const [pending2, startTransition2] = useTransition();

const loadExpensiveA = () => {
startTransition1(() => {
const result = expensiveComputationA();
setData(prev => ({ ...prev, a: result }));
});
};

const loadExpensiveB = () => {
startTransition2(() => {
const result = expensiveComputationB();
setData(prev => ({ ...prev, b: result }));
});
};

return (
<div>
<button onClick={loadExpensiveA} disabled={pending1}>
{pending1 ? 'Loading A...' : 'Load A'}
</button>
<button onClick={loadExpensiveB} disabled={pending2}>
{pending2 ? 'Loading B...' : 'Load B'}
</button>
</div>
);
}

Each transition has its own isPending flag, so you can show independent loading states.

Key Takeaways

  • useTransition marks a state update as low-priority, allowing React to interrupt it for user input.
  • startTransition wraps a function containing state updates; those updates are marked as transitions.
  • isPending is true while the transition is running—use it to show loading feedback.
  • The input/button that triggers the transition should be wrapped in the high-priority part; the expensive state update goes in startTransition.
  • useTransition is optional and only beneficial if the wrapped render is genuinely expensive (>50 ms).

Frequently Asked Questions

Can I use useTransition with async functions?

Not directly. startTransition expects a synchronous function. For async work (API calls), complete the async call first, then call startTransition when you update state with the results.

What if I forget to call startTransition for an expensive update?

React will render synchronously, blocking the UI until the render completes. The component will feel frozen. Always wrap expensive state updates in startTransition.

Does useTransition work with all state hooks?

Yes, useTransition works with useState, useReducer, and any hook that updates state. It's agnostic to the state mechanism.

Can a transition be interrupted by another transition?

Yes. React prioritizes user interactions (clicks, keyboard) above all transitions. If two transitions are running and the user interacts, React cancels both and handles the interaction first.

Further Reading