Eden TanStack Query
Guides

Mutations

Create, update, and delete data with type-safe mutations

Mutations

Any route using POST, PUT, PATCH, or DELETE is treated as a mutation procedure. Use mutationOptions() to create options for TanStack Query's useMutation.

Basic Mutation

import { useMutation } from '@tanstack/react-query'
import { useEden } from './lib/eden'

function CreateUserForm() {
  const eden = useEden()

  const createUser = useMutation(
    eden.users.post.mutationOptions()
  )

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    createUser.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    })
  }

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

Cache Invalidation

Invalidate related queries after a successful mutation using queryKey() or queryFilter():

import { useQueryClient, useMutation } from '@tanstack/react-query'

function CreateUserForm() {
  const eden = useEden()
  const queryClient = useQueryClient()

  const createUser = useMutation({
    ...eden.users.post.mutationOptions(),
    onSuccess: () => {
      // Using queryKey
      queryClient.invalidateQueries({
        queryKey: eden.users.get.queryKey()
      })

      // Or using queryFilter
      queryClient.invalidateQueries(
        eden.users.get.queryFilter()
      )
    },
  })
}

Invalidate multiple related queries:

const createPost = useMutation({
  ...eden.posts.post.mutationOptions(),
  onSuccess: (newPost) => {
    queryClient.invalidateQueries({ queryKey: eden.posts.get.queryKey() })
    queryClient.invalidateQueries({
      queryKey: eden.users({ id: newPost.authorId }).posts.get.queryKey()
    })
  },
})

Optimistic Updates

Update the UI immediately while the mutation runs in the background.

Pass the lifecycle callbacks inside mutationOptions(). Spreading the options and adding onMutate at the useMutation call site does not compile: the spread pins the mutation's context type before onMutate can infer it, so the context argument of onError becomes never.

const toggleTodo = useMutation(
  eden.todos({ id: todo.id }).patch.mutationOptions({
    onMutate: async (newTodo) => {
      await queryClient.cancelQueries({ queryKey: eden.todos.get.queryKey() })

      const previousTodos = queryClient.getQueryData(eden.todos.get.queryKey())

      queryClient.setQueryData(
        eden.todos.get.queryKey(),
        (old: Todo[] | undefined) =>
          old?.map(t => t.id === todo.id ? { ...t, completed: !t.completed } : t)
      )

      return { previousTodos }
    },

    onError: (_err, _newTodo, context) => {
      queryClient.setQueryData(eden.todos.get.queryKey(), context?.previousTodos)
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: eden.todos.get.queryKey() })
    },
  })
)

Mutation Key

Get the mutation key for tracking mutation state:

import { useMutationState } from '@tanstack/react-query'

const eden = useEden()

// Get mutation key
const key = eden.users.post.mutationKey()

// Track pending mutations
const pendingMutations = useMutationState({
  filters: {
    mutationKey: eden.users.post.mutationKey(),
    status: 'pending',
  },
})

Path Parameters

Path parameters work the same way as with queries:

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

updateUser.mutate({ name: 'New Name' })

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

deleteUser.mutate()

mutateAsync

Use mutateAsync for promise-based control flow:

const createUser = useMutation(eden.users.post.mutationOptions())

async function handleSubmit(data: CreateUserInput) {
  try {
    const user = await createUser.mutateAsync(data)
    navigate(`/users/${user.id}`)
  } catch (error) {
    console.error('Failed:', error)
  }
}

On this page