Skip to content

Mutation Options

mutationOptions() generates TanStack Query mutation options from your Eden Treaty routes.

Basic Usage

tsx
import { useMutation } from '@tanstack/react-query'
import { useTreaty } from './lib/treaty'

function CreateUser() {
  const treaty = useTreaty()

  const createUser = useMutation(
    treaty.api.users.mutationOptions()
  )

  const handleSubmit = (name: string) => {
    createUser.mutate({ name })
  }

  return (
    <form onSubmit={(e) => {
      e.preventDefault()
      handleSubmit(e.target.name.value)
    }}>
      <input name="name" placeholder="Name" />
      <button type="submit" disabled={createUser.isPending}>
        {createUser.isPending ? 'Creating...' : 'Create User'}
      </button>
    </form>
  )
}

HTTP Methods

By default, mutationOptions() uses POST. For other methods, pass the method name:

tsx
const treaty = useTreaty()

// POST (default)
const createUser = useMutation(
  treaty.api.users.mutationOptions()
)

// PUT
const updateUser = useMutation(
  treaty.api.users({ id: userId }).mutationOptions('put')
)

// PATCH
const patchUser = useMutation(
  treaty.api.users({ id: userId }).mutationOptions('patch')
)

// DELETE
const deleteUser = useMutation(
  treaty.api.users({ id: userId }).mutationOptions('delete')
)

With Cache Invalidation

Use pathFilter() for cache invalidation:

tsx
const treaty = useTreaty()
const queryClient = useQueryClient()

const createUser = useMutation(
  treaty.api.users.mutationOptions({
    onSuccess() {
      // Invalidate all queries under /api/users
      queryClient.invalidateQueries(treaty.api.users.pathFilter())
    }
  })
)

Optimistic Updates

tsx
const treaty = useTreaty()
const queryClient = useQueryClient()

const updateTask = useMutation(
  treaty.api.tasks({ id: taskId }).mutationOptions({
    async onMutate(variables) {
      // Cancel outgoing refetches
      await queryClient.cancelQueries(treaty.api.tasks.pathFilter())

      // Snapshot previous value
      const previousTasks = queryClient.getQueryData(
        treaty.api.tasks.queryKey()
      )

      // Optimistically update
      queryClient.setQueryData(
        treaty.api.tasks.queryKey(),
        (old: Task[] | undefined) =>
          old?.map(t => t.id === taskId ? { ...t, ...variables } : t)
      )

      return { previousTasks }
    },

    onError(err, variables, context) {
      // Rollback on error
      queryClient.setQueryData(
        treaty.api.tasks.queryKey(),
        context?.previousTasks
      )
    },

    onSettled() {
      // Refetch after mutation
      queryClient.invalidateQueries(treaty.api.tasks.pathFilter())
    }
  })
)

Mutation Variables

The mutation variables are fully typed based on your Elysia route:

tsx
// Server route with body schema
app.post('/api/users', ({ body }) => ({ id: 1, ...body }), {
  body: t.Object({
    name: t.String(),
    email: t.String()
  })
})

// Client - TypeScript enforces the shape
const createUser = useMutation(treaty.api.users.mutationOptions())

// ✅ Correct
createUser.mutate({ name: 'Alice', email: 'alice@example.com' })

// ❌ TypeScript error - missing email
createUser.mutate({ name: 'Alice' })

Overriding Options

Pass callbacks and options:

tsx
const createUser = useMutation(
  treaty.api.users.mutationOptions({
    onSuccess(data) {
      toast.success(`Created user: ${data.name}`)
      navigate(`/users/${data.id}`)
    },

    onError(error) {
      toast.error(`Failed: ${error.message}`)
    },

    retry: 3,
    retryDelay: 1000
  })
)

Delete with Confirmation

tsx
function DeleteButton({ taskId }: { taskId: string }) {
  const treaty = useTreaty()
  const queryClient = useQueryClient()

  const deleteTask = useMutation(
    treaty.api.tasks({ id: taskId }).mutationOptions('delete', {
      onSuccess() {
        queryClient.invalidateQueries(treaty.api.tasks.pathFilter())
        toast.success('Task deleted')
      }
    })
  )

  const handleDelete = () => {
    if (confirm('Delete this task?')) {
      deleteTask.mutate(undefined)
    }
  }

  return (
    <button onClick={handleDelete} disabled={deleteTask.isPending}>
      {deleteTask.isPending ? 'Deleting...' : 'Delete'}
    </button>
  )
}

Released under the Apache 2.0 License.