React Query v5 removed the onSuccess, onError, and onSettled callbacks from useQuery to avoid inconsistent behavior and ensure predictable side effects. These callbacks only fired on fetch, not when data came from cache, causing synchronization bugs.
This error occurs when upgrading to React Query (TanStack Query) v5 and attempting to use the onSuccess, onError, or onSettled callback functions within the useQuery hook. These callbacks have been completely removed from queries in v5, though they remain available for mutations. The React Query team removed these callbacks because they created inconsistent and unpredictable behavior. The callbacks would only fire when a fetch occurred, but not when data was retrieved from cache. This led to synchronization bugs where your side effects would execute in some scenarios but not others, making issues very difficult to trace. The removal aligns React Query better with React's declarative principles. Instead of imperative callbacks tied to fetch events, you should now use React's useEffect hook to respond to data changes, which provides more consistent and predictable behavior across all data sources (fetches, cache, refetches).
The recommended approach is to use React's useEffect hook with the data as a dependency. React Query ensures stable data through structural sharing, so the effect only runs when data actually changes, not on every background refetch.
Before (v4):
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {
console.log('Data loaded:', data);
toast.success('Todos loaded successfully');
}
});After (v5):
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
useEffect(() => {
if (data) {
console.log('Data loaded:', data);
toast.success('Todos loaded successfully');
}
}, [data]);This pattern works consistently whether data comes from a fetch, cache, or refetch.
For error-related side effects, check the error state in a useEffect:
Before (v4):
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onError: (error) => {
console.error('Query failed:', error);
toast.error(error.message);
}
});After (v5):
const { data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
useEffect(() => {
if (error) {
console.error('Query failed:', error);
toast.error(error.message);
}
}, [error]);For side effects that should run once per query regardless of which component calls it, configure global callbacks when setting up your QueryClient. This is particularly useful for logging, analytics, or global error handling.
import { QueryClient, QueryCache } from '@tanstack/react-query';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onSuccess: (data, query) => {
console.log('Global success:', query.queryKey);
},
onError: (error, query) => {
console.error('Global error:', query.queryKey, error);
// Send to error tracking service
},
}),
});
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app */}
</QueryClientProvider>
);
}Global callbacks execute once per query and avoid closure issues since they exist outside component scope.
When manually triggering refetches, handle success and error using standard try-catch blocks:
Before (v4):
const { refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
enabled: false,
onSuccess: (data) => console.log('Refetch succeeded:', data),
onError: (error) => console.error('Refetch failed:', error),
});
// Later
refetch();After (v5):
const { refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
enabled: false,
});
// Later
const handleRefresh = async () => {
try {
const { data } = await refetch();
console.log('Refetch succeeded:', data);
toast.success('Data refreshed');
} catch (error) {
console.error('Refetch failed:', error);
toast.error('Failed to refresh data');
}
};React Query v5 includes a codemod to help migrate callback usage automatically:
npx @tanstack/query-codemods@latest v5/remove-overloadsThis will attempt to convert your callback-based code to the new patterns. However, it's a best-effort tool and may not catch all cases, especially complex ones involving custom hooks or conditional logic.
After running the codemod:
1. Review all changes carefully
2. Test affected queries thoroughly
3. Manually migrate any missed or complex cases
4. Update TypeScript types if using custom query wrappers
Why Mutations Still Have Callbacks: The useMutation hook retains onSuccess, onError, and onSettled callbacks because mutations are imperative by nature. You explicitly trigger them with mutate() or mutateAsync(), so callbacks tied to that action make sense. Queries are declarative and can resolve from multiple sources (fetch, cache, refetch), making callbacks ambiguous.
Stale Closure Problem: The old callbacks created closure issues where the callback would capture values from the render when the query was created, but might execute much later with stale data. useEffect with proper dependencies solves this by always having access to current values.
Structural Sharing: React Query uses structural sharing to maintain referential equality for unchanged data. This means useEffect won't fire unnecessarily on background refetches if the data shape hasn't actually changed, giving you similar optimization to the old callbacks.
Performance Considerations: Using useEffect for every query doesn't create performance issues. React Query already optimizes renders through structural sharing, and useEffect only runs when dependencies change. For truly expensive operations, wrap your side effect logic in useMemo or useCallback.
Migration Timeline: This is a hard breaking change with no backward compatibility. If you have a large codebase with many queries using callbacks, consider:
1. Staying on v4 until you can dedicate time to migrate
2. Using the codemod as a starting point but plan for manual review
3. Migrating incrementally by feature area rather than all at once
4. Adding tests before migrating to catch behavioral changes
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
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite