# useMutation

Create, update, and delete data with type-safe mutations and automatic variable inference.

## Signature

Access `useMutation` through the query caller's dot-notation API. Variables are passed to `mutate()`, not to the hook:

```ts
const result = q.users.useMutation("create", options)

// q.<group>.useMutation(method, options?)
// group   — endpoint group name (e.g. "users")
// method  — method name within the group (e.g. "create")
// options — { requestOptions?, ...reactQueryMutationOptions }

// Returns: UseMutationResult with typed mutate function
// result.mutate(variables)   — fire and forget
// result.mutateAsync(variables) — returns Promise<R>
```

> **Variables go to mutate(), not the hook**
>
> Unlike `useQuery` where variables are passed in the options object, `useMutation` receives variables through the `mutate(variables)` call. This follows the standard React Query mutation pattern.

## Create mutation

The most common pattern — submit a form and create a resource. The `mutate` function is typed based on the method's variable type:

```ts title="components/create-user-form.tsx"
"use client"

import { q } from "@/lib/query"

export function CreateUserForm() {
  // Method<User, { name: string; email: string; role: string }>
  const { mutate, isPending } = q.users.useMutation("create", {
    onSuccess: (user) => {
      console.log("Created:", user.name) // ← user is typed as User
    },
    onError: (error) => {
      console.error("Failed:", error.message)
    },
  })

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const form = new FormData(e.currentTarget)

    mutate({
      name: form.get("name") as string,
      email: form.get("email") as string,
      role: "user",
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={isPending}>
        {isPending ? "Creating..." : "Create User"}
      </button>
    </form>
  )
}
```

## Update mutation

For updates, pass all required fields (including the ID for path parameters) to `mutate()`. Courier extracts path parameters automatically:

```ts title="components/edit-user-form.tsx"
"use client"

import { q } from "@/lib/query"

export function EditUserForm({ user }: { user: User }) {
  // Method<User, { id: number; name: string; email: string }>
  const { mutate, isPending } = q.users.useMutation("update")

  const handleSave = (name: string, email: string) => {
    // Variables passed to mutate(), not to the hook options
    mutate({
      id: user.id,
      name,
      email,
    })
    // ^? TypeScript enforces { id: number; name: string; email: string }
  }

  return (
    <button onClick={() => handleSave("Jane", "jane@test.com")} disabled={isPending}>
      Save
    </button>
  )
}
```

## Delete mutation

Delete methods typically return `null` and only need an ID. The variable type is inferred from the method definition:

```ts title="components/delete-user-button.tsx"
"use client"

import { q } from "@/lib/query"

export function DeleteUserButton({ userId }: { userId: number }) {
  // Method<null, { id: number }> → mutate takes { id: number }
  const { mutate, isPending } = q.users.useMutation("remove", {
    onSuccess: () => {
      console.log("User deleted")
    },
  })

  return (
    <button onClick={() => mutate({ id: userId })} disabled={isPending}>
      {isPending ? "Deleting..." : "Delete"}
    </button>
  )
}
```

## Cache invalidation

After a mutation succeeds, invalidate related queries to trigger a refetch. Use the hierarchical query key format `[source, group, method?, variables?]`:

```ts title="components/create-user-form.tsx"
"use client"

import { useQueryClient } from "@noverax/courier-react"
import { q } from "@/lib/query"

export function CreateUserForm() {
  const queryClient = useQueryClient()

  const { mutate } = q.users.useMutation("create", {
    onSuccess: () => {
      // Invalidate a specific method
      queryClient.invalidateQueries({ queryKey: ["api", "users", "getAll"] })

      // Or invalidate the entire users group
      // queryClient.invalidateQueries({ queryKey: ["api", "users"] })
    },
  })

  return (
    <button onClick={() => mutate({ name: "Jane", email: "jane@test.com", role: "user" })}>
      Create
    </button>
  )
}
```

> **Group-level invalidation**
>
> Use `["api", "users"]` to invalidate all queries in the users group at once — `getAll`, `getById`, and any other method. Use `["api"]` to invalidate every query for that source.

## Optimistic updates

For instant UI feedback, update the cache optimistically before the server responds. Roll back on error, and always refetch on settle to ensure consistency:

```ts title="components/user-manager.tsx"
"use client"

import { useQueryClient } from "@noverax/courier-react"
import { q } from "@/lib/query"

type User = { id: number; name: string; email: string }

export function UserManager() {
  const queryClient = useQueryClient()
  const { data: users } = q.users.useQuery("getAll")

  const { mutate: updateUser } = q.users.useMutation("update", {
    // 1. Cancel outgoing refetches so they don't overwrite our optimistic update
    onMutate: async (variables) => {
      await queryClient.cancelQueries({ queryKey: ["api", "users", "getAll"] })

      // 2. Snapshot the previous value
      const previous = queryClient.getQueryData<User[]>(["api", "users", "getAll"])

      // 3. Optimistically update the cache
      queryClient.setQueryData<User[]>(["api", "users", "getAll"], (old) =>
        old?.map((u) => (u.id === variables.id ? { ...u, ...variables } : u))
      )

      return { previous }
    },

    // 4. On error, roll back to the previous value
    onError: (_error, _variables, context) => {
      if (context?.previous) {
        queryClient.setQueryData(["api", "users", "getAll"], context.previous)
      }
    },

    // 5. Always refetch after error or success to ensure consistency
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["api", "users", "getAll"] })
    },
  })

  return (
    <ul>
      {users?.map((user) => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => updateUser({ id: user.id, name: "Updated", email: user.email })}>
            Update
          </button>
        </li>
      ))}
    </ul>
  )
}
```

What `onMutate` returns reaches `onError` and `onSettled` typed as it was returned, so `context.previous` above is a `User[] | undefined`. TypeScript reads the object from top to bottom: write `onMutate` before the callbacks that use its result.

## Next steps

- [useQuery](/docs/react-query/use-query) — fetch data with type-safe queries
- [Advanced Patterns](/docs/react-query/advanced) — prefetching, hydration, infinite queries, and more
- [Server Actions](/docs/server-side/actions) — mutations using Server Actions instead of proxy mode
- [Error Handling](/docs/core-concepts/error-handling) — handling mutation errors and HTTP status codes
