Real-time Subscriptions
Convex provides automatic real-time updates with zero configuration. When data changes in the database, every client subscribed to an affected query receives fresh results immediately.
How reactivity works
- The client calls
useQuery(api.tasks.list)and receives initial results. - Convex tracks which tables and indexes the query read.
- When a mutation modifies any of those tables, Convex re-executes the query server-side.
- If the result differs from the previous run, the new data is pushed to every subscribed client.
- React re-renders the component with the updated data.
There is no polling, no WebSocket management, and no cache invalidation logic to write.
Using the useQuery hook
"use client";
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
export function TaskList({ projectId }: { projectId: string }) {
const tasks = useQuery(api.tasks.listByProject, { projectId });
if (tasks === undefined) {
return <p>Loading...</p>;
}
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.title}</li>
))}
</ul>
);
}
useQueryreturnsundefinedwhile the first result is loading.- Once loaded, it returns the query result and keeps it up to date automatically.
- Pass
"skip"as the second argument to conditionally disable the subscription.
Optimistic updates
Mutations can define optimistic updates so the UI reflects changes before the server confirms them.
const createTask = useMutation(api.tasks.create).withOptimisticUpdate(
(localStore, args) => {
const current = localStore.getQuery(api.tasks.listByProject, {
projectId: args.projectId,
});
if (current !== undefined) {
localStore.setQuery(api.tasks.listByProject, { projectId: args.projectId }, [
...current,
{ _id: crypto.randomUUID(), ...args, tags: [], metadata: {} },
]);
}
}
);
Subscription patterns
| Pattern | When to use |
|---|---|
| Single query | Displaying a list or detail view |
| Conditional query | Only subscribe when a value is available (useQuery(fn, id ? { id } : "skip")) |
| Dependent queries | Chain queries where one provides args for the next |
| Paginated query | Use usePaginatedQuery for infinite scroll or page-based lists |
Tips
- Avoid broad queries that read entire tables — use indexes to limit the scope.
- Keep query functions fast; they re-run on every relevant mutation.
- Use
usePaginatedQueryfor large datasets to limit the amount of data transferred.