Problem
Building real-time UIs with traditional databases requires manually wiring up WebSockets, polling, or server-sent events to keep the client in sync. Every query needs explicit subscription logic, cache invalidation, and optimistic update handling. This leads to complex state management code that is brittle and hard to maintain.
Solution
Use Convex as your backend where queries are reactive by default. Data fetched with useQuery automatically updates when the underlying data changes.
Define a query and mutation:
// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("tasks").order("desc").collect();
},
});
export const create = mutation({
args: { text: v.string() },
handler: async (ctx, args) => {
await ctx.db.insert("tasks", {
text: args.text,
completed: false,
createdAt: Date.now(),
});
},
});
export const toggle = mutation({
args: { id: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.id);
if (!task) throw new Error("Task not found");
await ctx.db.patch(args.id, { completed: !task.completed });
},
});
Use reactive queries in React:
// src/app/page.tsx
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
export default function TaskList() {
const tasks = useQuery(api.tasks.list);
const createTask = useMutation(api.tasks.create);
const toggleTask = useMutation(api.tasks.toggle);
return (
<div>
<button onClick={() => createTask({ text: "New task" })}>Add</button>
{tasks?.map((task) => (
<div key={task._id} onClick={() => toggleTask({ id: task._id })}>
{task.completed ? "done" : "todo"}: {task.text}
</div>
))}
</div>
);
}
No WebSocket setup, no polling, no cache invalidation. When any client calls a mutation, all connected clients see the update immediately.
Why It Works
Convex tracks which queries depend on which data. When a mutation modifies a row, Convex re-runs affected queries and pushes updated results to all subscribed clients. This is handled at the infrastructure level, not in application code. TypeScript types are generated from the schema, providing end-to-end type safety.
Context
- Convex Chef (chef.convex.dev) scaffolds a full Convex project from a prompt, powered by Bolt
- Alternatives for reactive data include ElectricSQL and Zero, which sync at the database level
- Convex is well-suited for prototyping and collaborative apps but may add latency for read-heavy workloads
- The reactive model eliminates an entire class of bugs related to stale data and manual cache management
- Convex runs as a hosted service with edge functions; for more control, consider running your own backend