Skip to main content

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:

ScenarioUse This
Need to show a loading spinner while filteringuseTransition (gives isPending)
Mark an update non-urgent but don't need UI feedbackstartTransition
Outside a component (utility function, event handler in a library)startTransition
Want to check if a transition is pending elsewhere in the treeuseTransition (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

  • startTransition is a function that marks state updates as low-priority without requiring a hook or isPending flag.
  • Use it when you don't need to show loading feedback or when calling from outside a component.
  • startTransition can be imported directly from React and called from any JavaScript context.
  • Combine startTransition with optimistic updates for responsive user experiences (update local state, then transition the server save).
  • Error handling for startTransition should 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.

Further Reading