How to fix Cannot use startTransition with async state updates in React
This error occurs in React 18 when trying to use async/await inside startTransition callbacks, causing state updates after await to lose their transition marking. React 18 requires synchronous functions for startTransition, though this limitation is resolved in React 19.
Quick fix
Fixes Cannot use startTransition with async state updates
import { startTransition, useState } from 'react';
function SearchComponent() {
const [results, setResults] = useState([]);
const handleSearch = async (query: string) => {
// Fetch can happen outside startTransition
const data = await fetchSearchResults(query);
// Wrap the state update in startTransition
startTransition(() => {
setResults(data);
});
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
<ResultsList results={results} />
</div>
);
}- Wrap post-await state updates in nested startTransition
- Use useTransition hook for better control
- Move async logic outside the transition callback
- Consider upgrading to React 19 for native async support
import { startTransition, useState } from 'react';function SearchComponent() {const [results, setResults] = useState([]);const handleSearch = async (query: string) => {// Fetch can happen outside startTransitionconst data = await fetchSearchResults(query);// Wrap the state update in startTransitionstartTransition(() => {setResults(data);});};return (<div><input onChange={(e) => handleSearch(e.target.value)} /><ResultsList results={results} /></div>);}Cannot use startTransition with async state updates
What this error means
In React 18, the `startTransition` API requires the callback function to be synchronous. React immediately executes this function and marks all state updates that happen during its synchronous execution as transitions. When you use `await` inside a `startTransition` callback, any state updates that occur after the `await` statement are no longer marked as transitions. This is a JavaScript limitation caused by React losing the scope of the async context. Once execution crosses an `await` boundary, React can no longer track which state updates should be treated as non-urgent transitions versus urgent updates. This means async operations like data fetching cannot be directly wrapped in `startTransition` in React 18. The React team acknowledged this as a known limitation that would be addressed in future versions. In React 19, this restriction has been lifted, and `startTransition` now supports async callbacks natively, allowing developers to perform asynchronous operations without workarounds.
Typical symptoms
- State updates after await statements are not marked as transitions
- UI becomes unresponsive during async operations that should be interruptible
- Console warnings about async functions in startTransition
- Transitions not working as expected with data fetching logic
- State updates after promises resolve block the main thread
Common causes
- Using async/await directly inside a startTransition callback in React 18
- Not wrapping post-await state updates in a nested startTransition call
- Expecting React 18 to track async context across await boundaries
- Attempting to mark asynchronous data fetching operations as transitions
- Missing the synchronous function requirement in React 18 documentation
How to fix "Cannot use startTransition with async state updates"
In React 18, manually wrap any state updates that occur after await statements in another startTransition call:
import { startTransition, useState } from 'react';
function SearchComponent() {
const [results, setResults] = useState([]);
const handleSearch = async (query: string) => {
// Fetch can happen outside startTransition
const data = await fetchSearchResults(query);
// Wrap the state update in startTransition
startTransition(() => {
setResults(data);
});
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
<ResultsList results={results} />
</div>
);
}This ensures the state update after the async operation is marked as a transition.
The useTransition hook provides isPending state and better control over async transitions:
import { useTransition, useState } from 'react';
function SearchComponent() {
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = async (query: string) => {
const data = await fetchSearchResults(query);
startTransition(() => {
setResults(data);
});
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}The isPending flag helps show loading states during transitions.
Keep the async operation outside and only wrap the state update:
import { startTransition, useState } from 'react';
function DataComponent() {
const [data, setData] = useState(null);
const loadData = async () => {
// Async work happens outside
const response = await fetch('/api/data');
const result = await response.json();
// Only the state update is in startTransition
startTransition(() => {
setData(result);
});
};
return (
<button onClick={loadData}>
Load Data
</button>
);
}This pattern separates concerns and makes the transition boundary clear.
React 19 removes this limitation entirely by supporting async callbacks in startTransition:
// React 19 - async functions work directly
import { startTransition, useState } from 'react';
function SearchComponent() {
const [results, setResults] = useState([]);
const handleSearch = (query: string) => {
startTransition(async () => {
// Async function works directly in React 19
const data = await fetchSearchResults(query);
setResults(data);
});
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
<ResultsList results={results} />
</div>
);
}If you're starting a new project or can upgrade, React 19 provides a cleaner API.
For search inputs and filters, useDeferredValue can be a simpler alternative:
import { useDeferredValue, useState, useEffect } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const [results, setResults] = useState([]);
useEffect(() => {
async function search() {
if (deferredQuery) {
const data = await fetchSearchResults(deferredQuery);
setResults(data);
}
}
search();
}, [deferredQuery]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ResultsList results={results} />
</div>
);
}This defers the expensive update without manually managing transitions.
Advanced notes
Why This Limitation Exists
JavaScript's async/await is implemented using promises and microtasks. When execution crosses an await boundary, the function is suspended and the call stack unwinds. React's transition tracking relies on synchronous call stack inspection, which breaks when the function is suspended. This is why React 18 cannot automatically track state updates after await statements.
Future AsyncContext Proposal
The TC39 AsyncContext proposal aims to solve this at the JavaScript language level by providing a way to propagate context across async boundaries. Once AsyncContext is standardized and available, React 18 could theoretically support async transitions without manual wrapping.
Performance Considerations
Transitions are meant for non-urgent updates that can be interrupted. If your async operation is critical (like form submission), don't use startTransition at all - let it be an urgent update. Use transitions only for UI updates that can wait, like filtering large lists or switching tabs.
Concurrent Rendering
startTransition is part of React's concurrent rendering features. It tells React that certain updates are less urgent and can be interrupted if more important work comes in. This improves perceived performance by keeping the UI responsive, but it requires React to know which updates are transitions - hence the synchronous requirement in React 18.
Testing Transition Behavior
When testing components that use startTransition, be aware that transitions may not complete synchronously. Use act() and await for proper test isolation:
import { act, render, screen } from '@testing-library/react';
test('handles transition updates', async () => {
render(<SearchComponent />);
await act(async () => {
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'query' }
});
// Wait for async transition to complete
await new Promise(resolve => setTimeout(resolve, 100));
});
expect(screen.getByText('Results')).toBeInTheDocument();
});Debugging Transitions
Use React DevTools Profiler with "Record why each component rendered" enabled to verify that your state updates are being marked as transitions. Non-transition updates will show as high-priority renders, while transition updates show as low-priority.
Frequently asked questions
- What does "Cannot use startTransition with async state updates" mean?
- In React 18, the startTransition API requires the callback function to be synchronous. React immediately executes this function and marks all state updates that happen during its synchronous execution as transitions. When you use await inside a startTransition callback, any state updates that occur after the await statement are no longer marked as transitions.
- How do I fix "Cannot use startTransition with async state updates"?
- This error occurs in React 18 when trying to use async/await inside startTransition callbacks, causing state updates after await to lose their transition marking. React 18 requires synchronous functions for startTransition, though this limitation is resolved in React 19.
- What causes "Cannot use startTransition with async state updates"?
- Using async/await directly inside a startTransition callback in React 18 Not wrapping post-await state updates in a nested startTransition call Expecting React 18 to track async context across await boundaries
- Is the "Cannot use startTransition with async state updates" error serious?
- It is a medium-severity error โ more of a blocker than a risk. The steps above resolve it.
Official resources & further reading
- ๐React Official Documentation
- ๐React Docs: startTransition(Official Docs)
- ๐React Docs: useTransition Hook(Official Docs)
- ๐ฌReact 18 Working Group: startTransition Discussion(Stack Overflow)