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 } = useConvexQuery(api.todos.list)
const createTodo = useConvexMutation(api.todos.create)

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

  try {
    await createTodo({ text: value })
    text.value = ''
  } catch {
    // createTodo.error contains the normalized public error for this attempt.
  }
}
</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(args) resolves with the generated mutation result and rejects with ConvexCallError on failure. Its readonly refs describe the newest invocation.

Next, either explore query guides or add authentication.