Convex Realtime
Build reactive applications with Convex's real-time subscriptions, optimistic updates, intelligent caching, and cursor-based pagination.
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
Primary: https://docs.convex.dev/client/react Optimistic Updates: https://docs.convex.dev/client/react/optimistic-updates Pagination: https://docs.convex.dev/database/pagination For broader context: https://docs.convex.dev/llms.txt Instructions How Convex Realtime Works Automatic Subscriptions - useQuery creates a subscription that updates automatically Smart Caching - Query results are cached and shared across components Consistency - All subscriptions see a consistent view of the database Efficient Updates - Only re-renders when relevant data changes Basic Subscriptions // React component with real-time data import { useQuery } from "convex/react"; import { api } from "../convex/_generated/api";
function TaskList({ userId }: { userId: Id<"users"> }) { // Automatically subscribes and updates in real-time const tasks = useQuery(api.tasks.list, { userId });
if (tasks === undefined) { return
return (
-
{tasks.map((task) => (
- {task.title} ))}
Conditional Queries import { useQuery } from "convex/react"; import { api } from "../convex/_generated/api";
function UserProfile({ userId }: { userId: Id<"users"> | null }) { // Skip query when userId is null const user = useQuery( api.users.get, userId ? { userId } : "skip" );
if (userId === null) { return
if (user === undefined) { return
return
Mutations with Real-time Updates import { useMutation, useQuery } from "convex/react"; import { api } from "../convex/_generated/api";
function TaskManager({ userId }: { userId: Id<"users"> }) { const tasks = useQuery(api.tasks.list, { userId }); const createTask = useMutation(api.tasks.create); const toggleTask = useMutation(api.tasks.toggle);
const handleCreate = async (title: string) => { // Mutation triggers automatic re-render when data changes await createTask({ title, userId }); };
const handleToggle = async (taskId: Id<"tasks">) => { await toggleTask({ taskId }); };
return (
-
{tasks?.map((task) => (
- handleToggle(task._id)}> {task.completed ? "✓" : "○"} {task.title} ))}
Optimistic Updates
Show changes immediately before server confirmation:
import { useMutation, useQuery } from "convex/react"; import { api } from "../convex/_generated/api"; import { Id } from "../convex/_generated/dataModel";
function TaskItem({ task }: { task: Task }) { const toggleTask = useMutation(api.tasks.toggle).withOptimisticUpdate( (localStore, args) => { const { taskId } = args; const currentValue = localStore.getQuery(api.tasks.get, { taskId });
if (currentValue !== undefined) {
localStore.setQuery(api.tasks.get, { taskId }, {
...currentValue,
completed: !currentValue.completed,
});
}
}
);
return (
Optimistic Updates for Lists import { useMutation } from "convex/react"; import { api } from "../convex/_generated/api";
function useCreateTask(userId: Id<"users">) { return useMutation(api.tasks.create).withOptimisticUpdate( (localStore, args) => { const { title, userId } = args; const currentTasks = localStore.getQuery(api.tasks.list, { userId });
if (currentTasks !== undefined) {
// Add optimistic task to the list
const optimisticTask = {
_id: crypto.randomUUID() as Id<"tasks">,
_creationTime: Date.now(),
title,
userId,
completed: false,
};
localStore.setQuery(api.tasks.list, { userId }, [
optimisticTask,
...currentTasks,
]);
}
}
); }
Cursor-Based Pagination // convex/messages.ts import { query } from "./_generated/server"; import { v } from "convex/values"; import { paginationOptsValidator } from "convex/server";
export const listPaginated = query({ args: { channelId: v.id("channels"), paginationOpts: paginationOptsValidator, }, handler: async (ctx, args) => { return await ctx.db .query("messages") .withIndex("by_channel", (q) => q.eq("channelId", args.channelId)) .order("desc") .paginate(args.paginationOpts); }, });
// React component with pagination import { usePaginatedQuery } from "convex/react"; import { api } from "../convex/_generated/api";
function MessageList({ channelId }: { channelId: Id<"channels"> }) { const { results, status, loadMore } = usePaginatedQuery( api.messages.listPaginated, { channelId }, { initialNumItems: 20 } );
return (
{status === "CanLoadMore" && (
<button onClick={() => loadMore(20)}>Load More</button>
)}
{status === "LoadingMore" && <div>Loading...</div>}
{status === "Exhausted" && <div>No more messages</div>}
</div>
); }
Infinite Scroll Pattern import { usePaginatedQuery } from "convex/react"; import { useEffect, useRef } from "react"; import { api } from "../convex/_generated/api";
function InfiniteMessageList({ channelId }: { channelId: Id<"channels"> }) { const { results, status, loadMore } = usePaginatedQuery( api.messages.listPaginated, { channelId }, { initialNumItems: 20 } );
const observerRef = useRef
useEffect(() => { if (observerRef.current) { observerRef.current.disconnect(); }
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && status === "CanLoadMore") {
loadMore(20);
}
});
if (loadMoreRef.current) {
observerRef.current.observe(loadMoreRef.current);
}
return () => observerRef.current?.disconnect();
}, [status, loadMore]);
return (
Multiple Subscriptions import { useQuery } from "convex/react"; import { api } from "../convex/_generated/api";
function Dashboard({ userId }: { userId: Id<"users"> }) { // Multiple subscriptions update independently const user = useQuery(api.users.get, { userId }); const tasks = useQuery(api.tasks.list, { userId }); const notifications = useQuery(api.notifications.unread, { userId });
const isLoading = user === undefined || tasks === undefined || notifications === undefined;
if (isLoading) { return
return (
Welcome, {user.name}
You have {tasks.length} tasks
{notifications.length} unread notifications
Examples Real-time Chat Application // convex/messages.ts import { query, mutation } from "./_generated/server"; import { v } from "convex/values";
export const list = query({ args: { channelId: v.id("channels") }, returns: v.array(v.object({ _id: v.id("messages"), _creationTime: v.number(), content: v.string(), authorId: v.id("users"), authorName: v.string(), })), handler: async (ctx, args) => { const messages = await ctx.db .query("messages") .withIndex("by_channel", (q) => q.eq("channelId", args.channelId)) .order("desc") .take(100);
// Enrich with author names
return Promise.all(
messages.map(async (msg) => {
const author = await ctx.db.get(msg.authorId);
return {
...msg,
authorName: author?.name ?? "Unknown",
};
})
);
}, });
export const send = mutation({ args: { channelId: v.id("channels"), authorId: v.id("users"), content: v.string(), }, returns: v.id("messages"), handler: async (ctx, args) => { return await ctx.db.insert("messages", { channelId: args.channelId, authorId: args.authorId, content: args.content, }); }, });
// ChatRoom.tsx import { useQuery, useMutation } from "convex/react"; import { api } from "../convex/_generated/api"; import { useState, useRef, useEffect } from "react";
function ChatRoom({ channelId, userId }: Props) {
const messages = useQuery(api.messages.list, { channelId });
const sendMessage = useMutation(api.messages.send);
const [input, setInput] = useState("");
const messagesEndRef = useRef
// Auto-scroll to bottom on new messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]);
const handleSend = async (e: React.FormEvent) => { e.preventDefault(); if (!input.trim()) return;
await sendMessage({
channelId,
authorId: userId,
content: input.trim(),
});
setInput("");
};
return (
<form onSubmit={handleSend}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
/>
<button type="submit">Send</button>
</form>
</div>
); }
Best Practices Never run npx convex deploy unless explicitly instructed Never run any git commands unless explicitly instructed Use "skip" for conditional queries instead of conditionally calling hooks Implement optimistic updates for better perceived performance Use usePaginatedQuery for large datasets Handle undefined state (loading) explicitly Avoid unnecessary re-renders by memoizing derived data Common Pitfalls Conditional hook calls - Use "skip" instead of if statements Not handling loading state - Always check for undefined Missing optimistic update rollback - Optimistic updates auto-rollback on error Over-fetching with pagination - Use appropriate page sizes Ignoring subscription cleanup - React handles this automatically References Convex Documentation: https://docs.convex.dev/ Convex LLMs.txt: https://docs.convex.dev/llms.txt React Client: https://docs.convex.dev/client/react Optimistic Updates: https://docs.convex.dev/client/react/optimistic-updates Pagination: https://docs.convex.dev/database/pagination
← 返回排行榜