This error occurs when you try to call React Query's useMutation hook outside of a functional component or custom hook. Like all React hooks, useMutation must follow the Rules of Hooks and can only be called at the top level of components.
This error appears when you attempt to use React Query's `useMutation` hook in an invalid context—typically outside of a React functional component or custom hook. React Query's `useMutation` is a React hook, which means it must follow React's Rules of Hooks. The Rules of Hooks state that hooks can only be called: 1. At the top level of a functional component 2. At the top level of a custom hook 3. Never inside loops, conditions, nested functions, or regular JavaScript functions When you try to call `useMutation` in a utility function, event handler outside of a component, or a class component, React Query will throw this error because it cannot access the necessary React context and lifecycle management.
The simplest fix is to call useMutation at the top level of a functional component:
// ❌ WRONG - calling in utility function
// utils/api.ts
export function createUser(data: UserData) {
const mutation = useMutation({ // Error: can't call hook here
mutationFn: (userData: UserData) => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData)
})
});
return mutation.mutate(data);
}
// ✅ CORRECT - calling in component
// components/UserForm.tsx
import { useMutation } from '@tanstack/react-query';
function UserForm() {
const mutation = useMutation({
mutationFn: (userData: UserData) => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData)
})
});
const handleSubmit = (data: UserData) => {
mutation.mutate(data);
};
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(formData);
}}>
{/* form fields */}
</form>
);
}If you need to share mutation logic across multiple components, wrap useMutation in a custom hook:
// hooks/useCreateUser.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface UserData {
name: string;
email: string;
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userData: UserData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
},
onSuccess: () => {
// Invalidate and refetch queries after successful mutation
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
}
// Usage in any component
function UserForm() {
const createUser = useCreateUser();
const handleSubmit = (data: UserData) => {
createUser.mutate(data, {
onSuccess: () => {
console.log('User created!');
}
});
};
return (/* form JSX */);
}If you genuinely need to execute a mutation outside of React's component tree (e.g., in a service worker, middleware, or Node.js script), you should use regular fetch or axios instead:
// utils/api.ts
// For non-React contexts, don't use React Query
export async function createUser(data: UserData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
}
// Then in your component, you can wrap this if needed:
function UserForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
return (/* form JSX */);
}React Query is designed for React component lifecycle management. There isn't much benefit to calling mutations outside of React since you lose automatic lifecycle handling, caching, and retry logic.
Ensure useMutation is called unconditionally at the top level:
// ❌ WRONG - conditional hook call
function MyComponent({ enableMutation }: { enableMutation: boolean }) {
if (enableMutation) {
const mutation = useMutation({ /* ... */ }); // Breaks Rules of Hooks
}
return <div>...</div>;
}
// ✅ CORRECT - always call hook, conditionally use it
function MyComponent({ enableMutation }: { enableMutation: boolean }) {
const mutation = useMutation({ /* ... */ });
const handleAction = () => {
if (enableMutation) {
mutation.mutate(data);
}
};
return <button onClick={handleAction}>Submit</button>;
}Also avoid calling hooks inside loops or nested functions:
// ❌ WRONG - hook in loop
function BatchProcessor({ items }: { items: Item[] }) {
items.forEach(item => {
const mutation = useMutation({ /* ... */ }); // Error!
});
}
// ✅ CORRECT - single hook, loop in the mutation logic
function BatchProcessor({ items }: { items: Item[] }) {
const mutation = useMutation({
mutationFn: async (items: Item[]) => {
return Promise.all(
items.map(item => processItem(item))
);
}
});
const handleProcess = () => {
mutation.mutate(items);
};
return <button onClick={handleProcess}>Process All</button>;
}Why React Query requires component context:
React Query's hooks rely on React's context system to access the QueryClient instance and React's component lifecycle to manage subscriptions, cleanup, and re-renders. When you call useMutation outside of a component, these systems are unavailable.
Sharing mutation state across components (v5+):
In React Query v5+, you can use useMutationState to observe mutation state across different components:
import { useMutationState } from '@tanstack/react-query';
function GlobalMutationStatus() {
const mutations = useMutationState({
filters: { mutationKey: ['createUser'], status: 'pending' }
});
if (mutations.length > 0) {
return <div>Creating user...</div>;
}
return null;
}Advanced pattern for optimistic updates:
When using mutations with optimistic updates, keep query-related logic (like cache updates) in useMutation callbacks, and UI-related logic (toasts, redirects) in .mutate callbacks:
const mutation = useMutation({
mutationFn: updateUser,
// Query-related: runs even if component unmounts
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ['users', newUser.id] });
const previousUser = queryClient.getQueryData(['users', newUser.id]);
queryClient.setQueryData(['users', newUser.id], newUser);
return { previousUser };
},
onError: (err, newUser, context) => {
queryClient.setQueryData(['users', newUser.id], context?.previousUser);
}
});
// In component:
mutation.mutate(userData, {
// UI-related: only runs if component is mounted
onSuccess: () => {
toast.success('User updated!');
navigate('/users');
}
});Using QueryClient directly (advanced):
In rare cases where you absolutely must execute a mutation outside React (like in a background worker), you can use QueryClient directly without hooks:
import { QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient();
// This bypasses React entirely
await queryClient.executeMutation({
mutationFn: () => fetch('/api/users', { method: 'POST', body: data })
});However, this loses most of React Query's benefits and should only be used in non-React contexts.
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