Claude Agent Skill · by Waynesutton

Convex Realtime

Install Convex Realtime skill for Claude Code from waynesutton/convexskills.

convexrealtimesubscriptionsoptimistic-updatespagination
Install
Terminal · npx
$npx skills add https://github.com/waynesutton/convexskills --skill convex-realtime
Works with Paperclip

How Convex Realtime fits into a Paperclip company.

Convex Realtime drops into any Paperclip agent that handles convex and realtime work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md443 lines
Expand
---name: convex-realtimedisplayName: Convex Realtimedescription: Patterns for building reactive apps including subscription management, optimistic updates, cache behavior, and paginated queries with cursor-based loadingversion: 1.0.0author: Convextags: [convex, realtime, subscriptions, optimistic-updates, pagination]--- # 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 1. **Automatic Subscriptions** - useQuery creates a subscription that updates automatically2. **Smart Caching** - Query results are cached and shared across components3. **Consistency** - All subscriptions see a consistent view of the database4. **Efficient Updates** - Only re-renders when relevant data changes ### Basic Subscriptions ```typescript// React component with real-time dataimport { 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 ```typescriptimport { 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 ```typescriptimport { 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: ```typescriptimport { 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 ```typescriptimport { 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 ```typescript// convex/messages.tsimport { 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);  },});``` ```typescript// React component with paginationimport { 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 ```typescriptimport { 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 ```typescriptimport { 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 ```typescript// convex/messages.tsimport { 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,    });  },});``` ```typescript// ChatRoom.tsximport { 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<HTMLDivElement>(null);   // 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 (    <div className="chat-room">      <div className="messages">        {messages?.map((msg) => (          <div key={msg._id} className="message">            <strong>{msg.authorName}:</strong> {msg.content}          </div>        ))}        <div ref={messagesEndRef} />      </div>            <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 1. **Conditional hook calls** - Use "skip" instead of if statements2. **Not handling loading state** - Always check for undefined3. **Missing optimistic update rollback** - Optimistic updates auto-rollback on error4. **Over-fetching with pagination** - Use appropriate page sizes5. **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