typescriptintermediate

useMutation Hook for Side Effects

Handle mutations (POST/PUT/DELETE) with loading, error, reset, and optimistic update support.

typescript
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.