Convex Realtime
Patterns for building reactive apps including subscription management, optimistic updates, cache behavior, and paginated queries with cursor-based loading
Specification
- Skill ID
- waynesutton/convexskills/convex-realtime
- Publisher
- waynesutton
- Repository
- convexskills
- Installs
- 0
- Files
- 4
- Synced
- Sep 23, 2026
How to use it
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
waynesutton/convexskills/convex-realtimeInstalls these files- SKILL.md
- agents/openai.yaml
- assets/large-logo.png
- assets/small-logo.svg
What this skill tells the agent
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 <div>Loading...</div>;
}
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.title}</li>
))}
</ul>
);
}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 <div>Select a user</div>;
}
if (user === undefined) {
return <div>Loading...</div>;
}
return <div>{user.name}</div>;
}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 (
<div>
<button onClick={() => handleCreate("New Task")}>Add Task</button>
<ul>
{tasks?.map((task) => (
<li key={task._id} onClick={() => handleToggle(task._id)}>
{task.completed ? "✓" : "○"} {task.title}
</li>
))}
</ul>
</div>
);
}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 (
<div onClick={() => toggleTask({ taskId: task._id })}>
{task.completed ? "✓" : "○"} {task.title}
</div>
);
}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 (
<div>
{results.map((message) => (
<div key={message._id}>{message.content}</div>
))}
{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<IntersectionObserver>();
const loadMoreRef = useRef<HTMLDivElement>(null);
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 (
<div>
{results.map((message) => (
<div key={message._id}>{message.content}</div>
))}
<div ref={loadMoreRef} style={{ height: 1 }} />
{status === "LoadingMore" && <div>Loading...</div>}
</div>
);
}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 <div>Loading...</div>;
}
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>You have {tasks.length} tasks</p>
<p>{notifications.length} unread notifications</p>
</div>
);
}Examples
Real-time Chat Application
// convex/messages.ts
import { query, mutation } from "./_generated/server";More from waynesutton
Use this skill in a project
Open any RiverX project, search the Skills panel for waynesutton/convexskills/convex-realtime, and install it in one click.
