Skip to main content

Optimistic Todo List

Create, toggle, and remove todos immediately with direct local-store updates and automatic rollback.

Result

Todo interactions update the currently loaded list before the mutation response. Convex confirmation replaces the optimistic view; failures restore the previous result.

Create

ts
const createTodo = useConvexMutation(api.todos.create, {
  optimisticUpdate: (store, args) => {
    const todos = store.getQuery(api.todos.list, {})
    if (todos === undefined) return undefined

    store.setQuery(api.todos.list, {}, [
      {
        _id: crypto.randomUUID() as Id<'todos'>,
        _creationTime: Date.now(),
        text: args.text,
        completed: false,
      },
      ...todos,
    ])
    return undefined
  },
})

Toggle

ts
const toggleTodo = useConvexMutation(api.todos.toggle, {
  optimisticUpdate: (store, args) => {
    const todos = store.getQuery(api.todos.list, {})
    if (todos === undefined) return undefined

    store.setQuery(
      api.todos.list,
      {},
      todos.map((todo) =>
        todo._id === args.todoId ? { ...todo, completed: !todo.completed } : todo,
      ),
    )
    return undefined
  },
})

Remove

ts
const removeTodo = useConvexMutation(api.todos.remove, {
  optimisticUpdate: (store, args) => {
    const todos = store.getQuery(api.todos.list, {})
    if (todos === undefined) return undefined

    store.setQuery(
      api.todos.list,
      {},
      todos.filter((todo) => todo._id !== args.todoId),
    )
    return undefined
  },
})

Security boundary

The backend loads the todo and checks ownership before toggling or deleting. The local predicate is presentation only.

Verify rollback

Temporarily make each mutation reject after validation and confirm:

  • a created temporary todo disappears;
  • a toggle returns to its server value;
  • a removed todo reappears;
  • the error is visible and the form remains usable.

Remove the forced failures after verification.