This React 18 error occurs when you call startTransition with a callback that does not perform any state updates. startTransition is designed to mark state updates as low-priority transitions, so it expects at least one setState call within its callback function.
This error appears when you use React 18's startTransition API with a callback function that doesn't actually trigger any state updates. The startTransition function is specifically designed to mark state updates as non-urgent "transitions" that can be interrupted by more critical updates, improving the user experience during expensive operations. React expects startTransition to wrap actual state changes (via useState's setter, useReducer's dispatch, or similar). If you pass a callback that doesn't contain any state update calls, React throws this error because there's no transition to perform. This is different from other React hooks where an empty callback might simply do nothing—startTransition validates that it's being used for its intended purpose. The error serves as a safeguard against misusing the API, such as wrapping non-state-updating logic or forgetting to include the actual setState call within the transition callback.
Check that your startTransition callback actually calls a state setter function:
import { startTransition, useState } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// ❌ WRONG - no state update inside startTransition
startTransition(() => {
console.log('Searching...');
});
// ✅ CORRECT - state update is inside startTransition
startTransition(() => {
setQuery(e.target.value);
});
};
return <input onChange={handleChange} />;
}Make sure the setState (or dispatch) call is directly inside the callback.
If your state update is happening outside the startTransition callback, move it inside:
// ❌ WRONG - state update outside transition
startTransition(() => {
// Empty callback
});
setResults(newResults);
// ✅ CORRECT - state update inside transition
startTransition(() => {
setResults(newResults);
});Only non-state operations (like measurements or calculations) should happen outside.
If you're not actually updating state, don't use startTransition:
// ❌ WRONG - startTransition with no state update
startTransition(() => {
console.log('Logging something');
trackAnalytics('event');
});
// ✅ CORRECT - just call the function directly
console.log('Logging something');
trackAnalytics('event');startTransition is only for marking state updates as transitions, not general function calls.
If your state update is conditional, ensure at least one branch updates state or remove startTransition:
// ❌ WRONG - might execute with no state update
startTransition(() => {
if (someCondition) {
setData(newData);
}
// If someCondition is false, no state update occurs
});
// ✅ OPTION 1 - Ensure state update always happens
startTransition(() => {
if (someCondition) {
setData(newData);
} else {
setData(defaultData); // Always update
}
});
// ✅ OPTION 2 - Move startTransition inside conditional
if (someCondition) {
startTransition(() => {
setData(newData);
});
}Choose the pattern that matches your intent.
For state updates inside async operations, wrap each update in its own startTransition:
// ❌ WRONG - async function as callback (React 18)
startTransition(async () => {
const data = await fetchData();
setData(data); // This won't be marked as transition
});
// ✅ CORRECT - Wrap the state update after async
const handleClick = async () => {
const data = await fetchData();
startTransition(() => {
setData(data); // Marked as transition
});
};
// Note: React 19 supports async startTransition directlyIn React 18, startTransition callbacks must be synchronous.
React 19 Improvements: In React 19, startTransition supports async functions natively, so you can use async/await inside the callback without manual wrapping. The requirement for synchronous callbacks was a React 18 limitation.
useTransition Alternative: If you need to show loading states during transitions, use the useTransition hook instead: const [isPending, startTransition] = useTransition();. This provides an isPending flag you can use to display loading UI.
Transition Scope: Only state updates called directly during the execution of the startTransition callback are marked as transitions. Updates in nested timeouts, promises, or event handlers need their own startTransition wrappers.
Performance Consideration: Don't wrap every state update in startTransition—only use it for updates that can be deferred (like filtering large lists or updating search results). Urgent updates (like text input in a controlled component) should remain synchronous.
Debugging Tip: If you're unsure whether your callback is executing, add a console.log immediately before the setState call to verify it's being reached. The error only triggers if React detects zero state updates during callback execution.
React.FC expects children prop to be defined
React.FC no longer includes implicit children prop
Warning: You provided a `selected` prop to a form field without an `onChange` handler
You provided a 'selected' prop without an onChange handler
Failed to load source map from suspense chunk
How to fix "Failed to load source map from suspense chunk" in React
Prop spreading could cause security issues
Prop spreading could cause security issues
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency