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
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
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
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.