startTransition API: React Non-Urgent Updates
startTransition is a React 19 function that wraps a state update, marking it as non-urgent and telling React to prioritize other work (like user input) above it. Unlike useTransition, which is a hook that also gives you an isPending flag, startTransition is a standalone API that you can call directly without managing a loading state. Use startTransition when you don't need to show visual feedback to the user (the update happens silently in the background) or when you're inside a non-component context like an event handler or utility function.
The core difference: useTransition = hook with isPending feedback; startTransition = bare function for marking updates without feedback. Both use the same underlying concurrent rendering mechanism.
startTransition vs. useTransition
Here's when to use each:
| Scenario | Use This |
|---|---|
| Need to show a loading spinner while filtering | useTransition (gives isPending) |
| Mark an update non-urgent but don't need UI feedback | startTransition |
| Outside a component (utility function, event handler in a library) | startTransition |
| Want to check if a transition is pending elsewhere in the tree | useTransition (pass isPending via context or props) |
startTransition is a function imported from React directly:
import { startTransition, useState } from 'react';
export function CommentThread() {
const [comments, setComments] = useState([]);
const handlePostComment = (text) => {
// Update comments immediately (optimistic update)
setComments(prev => [...prev, { id: Date.now(), text }]);
// Debounce the server save as a transition (no feedback needed)
startTransition(() => {
saveCommentToServer(text);
});
};
return (
<div>
<button onClick={() => handlePostComment('Hello')}>
Post Comment
</button>
<ul>
{comments.map(c => <li key={c.id}>{c.text}</li>)}
</ul>
</div>
);
}
function saveCommentToServer(text) {
// Expensive server operation (might take 1–2 seconds)
return fetch('/api/comments', { method: 'POST', body: JSON.stringify({ text }) });
}
In this example, the optimistic update (adding the comment to the local list) happens immediately, and the server save is marked as a transition. The user sees their comment instantly, even if the server is slow.
Calling startTransition Outside Components
startTransition can be called from any JavaScript context, not just inside a component body. This is powerful for event handlers, middleware, or utility functions:
import { startTransition } from 'react';
// Example: Update state from an event handler
export function setupGlobalSearch(setResults) {
document.addEventListener('input', (e) => {
if (e.target.id === 'global-search') {
const query = e.target.value;
startTransition(() => {
const results = performGlobalSearch(query);
setResults(results);
});
}
});
}
function performGlobalSearch(query) {
// Simulate expensive search across entire app state
return Array.from({ length: 100000 }, (_, i) => ({
id: i,
title: `Result ${i}`,
match: Math.random() > 0.99,
})).filter(r => r.match);
}
// In your app:
// const [results, setResults] = useState([]);
// useEffect(() => setupGlobalSearch(setResults), []);
Here, startTransition is called from a native DOM event listener, not from React. This is a legitimate pattern when you need to integrate React state updates with browser APIs or external libraries.
Bare startTransition with No Feedback
When you don't need to show loading state, use startTransition to silently deprioritize work:
import { startTransition, useState } from 'react';
export function NotificationCenter() {
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const handleMarkAllRead = () => {
// Update unread count immediately (user-facing feedback)
setUnreadCount(0);
// Update notification list as a transition (no UI feedback needed)
startTransition(() => {
const updated = notifications.map(n => ({ ...n, read: true }));
setNotifications(updated);
});
};
return (
<div>
<p>Unread: {unreadCount}</p>
<button onClick={handleMarkAllRead}>Mark All Read</button>
<ul>
{notifications.map(n => (
<li key={n.id} style={{ opacity: n.read ? 0.5 : 1 }}>
{n.message}
</li>
))}
</ul>
</div>
);
}
The unread count updates instantly, while the expensive re-render of the notification list happens as a low-priority transition.
Combining startTransition with Error Handling
For server updates or async work, wrap the state update in startTransition but handle errors outside:
import { startTransition, useState } from 'react';
export function TodoApp() {
const [todos, setTodos] = useState([]);
const [error, setError] = useState(null);
const addTodo = async (title) => {
try {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ title }),
});
if (!response.ok) {
setError('Failed to add todo');
return;
}
const newTodo = await response.json();
// Mark the state update (adding to local list) as a transition
startTransition(() => {
setTodos(prev => [...prev, newTodo]);
});
} catch (err) {
setError(err.message);
}
};
return (
<div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button onClick={() => addTodo('Buy milk')}>Add Todo</button>
<ul>
{todos.map(t => <li key={t.id}>{t.title}</li>)}
</ul>
</div>
);
}
The async fetch completes first, then the state update is wrapped in startTransition. This ensures errors are caught and displayed immediately, while the UI update is deprioritized.
Key Takeaways
startTransitionis a function that marks state updates as low-priority without requiring a hook orisPendingflag.- Use it when you don't need to show loading feedback or when calling from outside a component.
startTransitioncan be imported directly from React and called from any JavaScript context.- Combine
startTransitionwith optimistic updates for responsive user experiences (update local state, then transition the server save). - Error handling for
startTransitionshould happen outside the transition callback.
Frequently Asked Questions
Can I call startTransition twice in the same event handler?
Yes, but only the most recent transition will run. If you call startTransition again before the first completes, React cancels the first and starts the second. Use useTransition with multiple hooks if you need independent transitions.
Does startTransition work with useReducer?
Yes. You can wrap a dispatch call in startTransition, and the state update will be deprioritized just as with useState.
What happens if I call startTransition without wrapping a state update?
Nothing happens. startTransition only has an effect if the callback triggers a React state update. If you call it around a non-state-updating operation, React ignores it.
Can I use startTransition with async/await inside the callback?
No. startTransition expects a synchronous function. Complete async work (fetches, timers) before calling startTransition, then update state synchronously inside the callback.