$npx -y skills add waynesutton/convexskills --skill convex-realtimePatterns for building reactive apps including subscription management, optimistic updates, cache behavior, and paginated queries with cursor-based loading
| 1 | # Convex Realtime |
| 2 | |
| 3 | Build reactive applications with Convex's real-time subscriptions, optimistic updates, intelligent caching, and cursor-based pagination. |
| 4 | |
| 5 | ## Documentation Sources |
| 6 | |
| 7 | Before implementing, do not assume; fetch the latest documentation: |
| 8 | |
| 9 | - Primary: https://docs.convex.dev/client/react |
| 10 | - Optimistic Updates: https://docs.convex.dev/client/react/optimistic-updates |
| 11 | - Pagination: https://docs.convex.dev/database/pagination |
| 12 | - For broader context: https://docs.convex.dev/llms.txt |
| 13 | |
| 14 | ## Instructions |
| 15 | |
| 16 | ### How Convex Realtime Works |
| 17 | |
| 18 | 1. **Automatic Subscriptions** - useQuery creates a subscription that updates automatically |
| 19 | 2. **Smart Caching** - Query results are cached and shared across components |
| 20 | 3. **Consistency** - All subscriptions see a consistent view of the database |
| 21 | 4. **Efficient Updates** - Only re-renders when relevant data changes |
| 22 | |
| 23 | ### Basic Subscriptions |
| 24 | |
| 25 | ```typescript |
| 26 | // React component with real-time data |
| 27 | import { useQuery } from "convex/react"; |
| 28 | import { api } from "../convex/_generated/api"; |
| 29 | |
| 30 | function TaskList({ userId }: { userId: Id<"users"> }) { |
| 31 | // Automatically subscribes and updates in real-time |
| 32 | const tasks = useQuery(api.tasks.list, { userId }); |
| 33 | |
| 34 | if (tasks === undefined) { |
| 35 | return <div>Loading...</div>; |
| 36 | } |
| 37 | |
| 38 | return ( |
| 39 | <ul> |
| 40 | {tasks.map((task) => ( |
| 41 | <li key={task._id}>{task.title}</li> |
| 42 | ))} |
| 43 | </ul> |
| 44 | ); |
| 45 | } |
| 46 | ``` |
| 47 | |
| 48 | ### Conditional Queries |
| 49 | |
| 50 | ```typescript |
| 51 | import { useQuery } from "convex/react"; |
| 52 | import { api } from "../convex/_generated/api"; |
| 53 | |
| 54 | function UserProfile({ userId }: { userId: Id<"users"> | null }) { |
| 55 | // Skip query when userId is null |
| 56 | const user = useQuery( |
| 57 | api.users.get, |
| 58 | userId ? { userId } : "skip" |
| 59 | ); |
| 60 | |
| 61 | if (userId === null) { |
| 62 | return <div>Select a user</div>; |
| 63 | } |
| 64 | |
| 65 | if (user === undefined) { |
| 66 | return <div>Loading...</div>; |
| 67 | } |
| 68 | |
| 69 | return <div>{user.name}</div>; |
| 70 | } |
| 71 | ``` |
| 72 | |
| 73 | ### Mutations with Real-time Updates |
| 74 | |
| 75 | ```typescript |
| 76 | import { useMutation, useQuery } from "convex/react"; |
| 77 | import { api } from "../convex/_generated/api"; |
| 78 | |
| 79 | function TaskManager({ userId }: { userId: Id<"users"> }) { |
| 80 | const tasks = useQuery(api.tasks.list, { userId }); |
| 81 | const createTask = useMutation(api.tasks.create); |
| 82 | const toggleTask = useMutation(api.tasks.toggle); |
| 83 | |
| 84 | const handleCreate = async (title: string) => { |
| 85 | // Mutation triggers automatic re-render when data changes |
| 86 | await createTask({ title, userId }); |
| 87 | }; |
| 88 | |
| 89 | const handleToggle = async (taskId: Id<"tasks">) => { |
| 90 | await toggleTask({ taskId }); |
| 91 | }; |
| 92 | |
| 93 | return ( |
| 94 | <div> |
| 95 | <button onClick={() => handleCreate("New Task")}>Add Task</button> |
| 96 | <ul> |
| 97 | {tasks?.map((task) => ( |
| 98 | <li key={task._id} onClick={() => handleToggle(task._id)}> |
| 99 | {task.completed ? "✓" : "○"} {task.title} |
| 100 | </li> |
| 101 | ))} |
| 102 | </ul> |
| 103 | </div> |
| 104 | ); |
| 105 | } |
| 106 | ``` |
| 107 | |
| 108 | ### Optimistic Updates |
| 109 | |
| 110 | Show changes immediately before server confirmation: |
| 111 | |
| 112 | ```typescript |
| 113 | import { useMutation, useQuery } from "convex/react"; |
| 114 | import { api } from "../convex/_generated/api"; |
| 115 | import { Id } from "../convex/_generated/dataModel"; |
| 116 | |
| 117 | function TaskItem({ task }: { task: Task }) { |
| 118 | const toggleTask = useMutation(api.tasks.toggle).withOptimisticUpdate( |
| 119 | (localStore, args) => { |
| 120 | const { taskId } = args; |
| 121 | const currentValue = localStore.getQuery(api.tasks.get, { taskId }); |
| 122 | |
| 123 | if (currentValue !== undefined) { |
| 124 | localStore.setQuery(api.tasks.get, { taskId }, { |
| 125 | ...currentValue, |
| 126 | completed: !currentValue.completed, |
| 127 | }); |
| 128 | } |
| 129 | } |
| 130 | ); |
| 131 | |
| 132 | return ( |
| 133 | <div onClick={() => toggleTask({ taskId: task._id })}> |
| 134 | {task.completed ? "✓" : "○"} {task.title} |
| 135 | </div> |
| 136 | ); |
| 137 | } |
| 138 | ``` |
| 139 | |
| 140 | ### Optimistic Updates for Lists |
| 141 | |
| 142 | ```typescript |
| 143 | import { useMutation } from "convex/react"; |
| 144 | import { api } from "../convex/_generated/api"; |
| 145 | |
| 146 | function useCreateTask(userId: Id<"users">) { |
| 147 | return useMutation(api.tasks.create).withOptimisticUpdate( |
| 148 | (localStore, args) => { |
| 149 | const { title, userId } = args; |
| 150 | const currentTasks = localStore.getQuery(api.tasks.list, { userId }); |
| 151 | |
| 152 | if (currentTasks !== undefined) { |
| 153 | // Add optimistic task to the list |
| 154 | const optimisticTask = { |
| 155 | _id: crypto.randomUUID() as Id<"tasks">, |
| 156 | _creationTime: Date.now(), |
| 157 | title, |
| 158 | userId, |
| 159 | completed: false, |
| 160 | }; |
| 161 | |
| 162 | localStore.setQuery(api.tasks.list, { userId }, [ |
| 163 | optimisticTask, |
| 164 | ...currentTasks, |
| 165 | ]); |
| 166 | } |
| 167 | } |
| 168 | ); |
| 169 | } |
| 170 | ``` |
| 171 | |
| 172 | ### Cursor-Based Pagination |
| 173 | |
| 174 | ```typescript |
| 175 | // convex/messages.ts |
| 176 | import { query } from "./_generated/server"; |
| 177 | import { v } from "convex/values"; |
| 178 | import { pag |