Skip to content

Quick start

This walks through one complete feature: a Convex table, a query and a mutation on the backend, and an Angular component that renders the query live and writes through the mutation.

It assumes you have already installed the packages and registered provideConvex().

convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
todos: defineTable({
title: v.string(),
completed: v.boolean(),
}),
});

A query reads data and is subscribable; a mutation writes data transactionally.

convex/todos.ts
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
export const list = query({
args: { count: v.number() },
handler: async (ctx, args) => {
return await ctx.db.query('todos').order('desc').take(args.count);
},
});
export const create = mutation({
args: { title: v.string() },
handler: async (ctx, args) => {
return await ctx.db.insert('todos', { title: args.title, completed: false });
},
});

With convex dev running, saving this file deploys both functions and regenerates convex/_generated/api.d.ts. The generated api object is what you pass to the helpers, and it is what makes argument and return types flow into your component with no manual annotations.

injectQuery() takes a function reference and a function returning the arguments. injectMutation() takes a function reference and returns a mutate() method plus reactive state.

todo-list.component.ts
import { Component, signal } from '@angular/core';
import { injectMutation, injectQuery } from 'convex-angular';
import { api } from './convex/api';
@Component({
selector: 'app-todo-list',
template: `
<form (submit)="add($event)">
<input name="title" [value]="title()" (input)="title.set($any($event.target).value)" />
<button type="submit" [disabled]="createTodo.isLoading()">
{{ createTodo.isLoading() ? 'Adding…' : 'Add todo' }}
</button>
</form>
@if (createTodo.error(); as error) {
<p role="alert">{{ error.message }}</p>
}
@switch (todos.status()) {
@case ('pending') {
<p>Loading todos…</p>
}
@case ('error') {
<p role="alert">{{ todos.error()?.message }}</p>
}
@case ('skipped') {
<p>Nothing to load.</p>
}
@case ('success') {
<ul>
@for (todo of todos.data() ?? []; track todo._id) {
<li>{{ todo.title }}</li>
} @empty {
<li>No todos yet.</li>
}
</ul>
}
}
`,
})
export class TodoListComponent {
readonly title = signal('');
// The argument function is reactive: reading `count()` inside it makes the
// subscription follow the signal.
readonly count = signal(20);
readonly todos = injectQuery(api.todos.list, () => ({ count: this.count() }));
readonly createTodo = injectMutation(api.todos.create, {
onSuccess: () => this.title.set(''),
});
async add(event: Event): Promise<void> {
event.preventDefault();
// No manual refetch: the live query subscription pushes the new todo.
await this.createTodo.mutate({ title: this.title() });
}
}
Expression Meaning
todos.status() 'pending' | 'success' | 'error' | 'skipped' — the single value to branch on.
todos.data() The query result, or undefined before the first value arrives.
todos.error() The subscription error, or undefined.
todos.isRefetching() True while resubscribing with a previous value still shown — render a subtle spinner.
todos.refetch() Force a resubscribe.
createTodo.mutate(args) Runs the mutation; resolves with its return value, rejects on failure.
createTodo.isLoading() True while the mutation is in flight.
createTodo.error() The last mutation error, or undefined.
createTodo.reset() Clears data, error, and returns the status to 'idle'.
  • Providers — where provideConvex() may be registered, and what it sets up.
  • Reactivity — how reactive arguments decide when to resubscribe.
  • Status and state — the exact status derivations and their gotchas.
  • Error handling — signals, promise rejections, and ConvexError.