typescriptintermediate
useMutation Hook for Side Effects
Handle mutations (POST/PUT/DELETE) with loading, error, reset, and optimistic update support.
typescriptPress ⌘/Ctrl + Shift + C to copy
import { useCallback, useState } from 'react';
interface MutationState<T> {
data: T | null;
error: Error | null;
loading: boolean;
}
interface MutationOptions<T, V> {
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
onSettled?: () => void;
}
function useMutation<T, V = void>(
mutationFn: (variables: V) => Promise<T>,
options?: MutationOptions<T, V>,
) {
const [state, setState] = useState<MutationState<T>>({
data: null,
error: null,
loading: false,
});
const mutate = useCallback(
async (variables: V) => {
setState({ data: null, error: null, loading: true });
try {
const data = await mutationFn(variables);
setState({ data, error: null, loading: false });
options?.onSuccess?.(data);
return data;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setState({ data: null, error, loading: false });
options?.onError?.(error);
throw error;
} finally {
options?.onSettled?.();
}
},
[mutationFn, options],
);
const reset = useCallback(() => {
setState({ data: null, error: null, loading: false });
}, []);
return { ...state, mutate, reset };
}
// Usage
function CreateUserForm() {
const { mutate, loading, error, data } = useMutation<
{ id: string; name: string },
{ name: string; email: string }
>(
async (vars) => {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(vars),
});
if (!res.ok) throw new Error('Failed to create user');
return res.json();
},
{
onSuccess: (user) => console.log('Created:', user.id),
onError: (err) => console.error(err.message),
},
);
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
await mutate({ name: fd.get('name') as string, email: fd.get('email') as string });
}}
className="space-y-3"
>
<input name="name" placeholder="Name" className="bg-gray-800 text-white px-3 py-2 rounded" />
<input name="email" placeholder="Email" className="bg-gray-800 text-white px-3 py-2 rounded" />
<button disabled={loading} className="bg-blue-600 text-white px-4 py-2 rounded">
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <p className="text-red-400">{error.message}</p>}
{data && <p className="text-green-400">Created: {data.name}</p>}
</form>
);
}
export { useMutation };Use Cases
- Form submission handling
- API mutation management
- Optimistic UI updates
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
typescriptbeginner
usePrevious Hook
Track the previous value of any state or prop using a ref-based hook for comparison logic.
Best for: Detecting value changes
#hooks#state
typescriptintermediate
React Custom Hook for Data Fetching
A reusable useFetch hook with loading states, error handling, caching, and abort support.
Best for: Reusable data fetching across components
#react#hooks
typescriptintermediate
React Form Validation Hook
A type-safe form validation hook with field-level errors, dirty tracking, and submit handling.
Best for: Type-safe form handling without libraries
#react#forms
typescriptintermediate
React Intersection Observer Hook
Custom useIntersectionObserver hook for lazy loading, infinite scroll, and scroll animations.
Best for: Lazy loading components when they enter viewport
#react#hooks