Skip to main content

Add a Mutation

Write Convex data and observe the live query update without a manual refetch.

Add a mutation to the todo app from the previous page.

Define the backend write

convex/todos.ts
import { mutation, query } from './_generated/server'
import { ConvexError, v } from 'convex/values'

export const list = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query('todos').withIndex('by_created').order('desc').take(50)
  },
})

export const create = mutation({
  args: {
    text: v.string(),
  },
  handler: async (ctx, args) => {
    const text = args.text.trim()
    if (!text || text.length > 200) {
      throw new ConvexError({
        code: 'INVALID_TODO_TEXT',
        message: 'Todo text must contain 1 to 200 characters',
      })
    }

    return await ctx.db.insert('todos', {
      text,
      completed: false,
      createdAt: Date.now(),
    })
  },
})

Connect the form

Replace the page with:

app/pages/index.vue
<script setup lang="ts">
import { api } from '#convex/api'

const text = ref('')
const { data: todos, status, error } = await useConvexQuery(api.todos.list, {})
const createTodo = useConvexMutation(api.todos.create)

async function submit() {
  const value = text.value.trim()
  if (!value) return

  const result = await createTodo.safe({ text: value })
  if (result.ok) text.value = ''
}
</script>

<template>
  <main>
    <h1>Todos</h1>

    <form @submit.prevent="submit">
      <input v-model="text" maxlength="200" aria-label="Todo text" />
      <button :disabled="createTodo.pending || !text.trim()">
        {{ createTodo.pending ? 'Adding…' : 'Add todo' }}
      </button>
    </form>

    <p v-if="createTodo.error">{{ createTodo.error.message }}</p>
    <p v-if="status === 'pending'">Loading todos…</p>
    <p v-else-if="error">Could not load todos.</p>

    <ul v-else>
      <li v-for="todo in todos ?? []" :key="todo._id">
        {{ todo.text }}
      </li>
    </ul>
  </main>
</template>

What updates the list

The mutation writes to todos. Convex recomputes subscribed queries whose dependencies changed and publishes the new todos.list result.

The module updates the query ref. The page does not call refresh() after the mutation.

createTodo.safe() returns:

ts
{ ok: true, data: todoId }

or:

ts
{ ok: false, error: ConvexCallError }

Calling createTodo(args) directly provides the same reactive state but rejects on failure.

Next, either explore query guides or add authentication.