convex-realtime

Build reactive applications with Convex's real-time subscriptions, optimistic updates, intelligent caching, and cursor-based pagination.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "convex-realtime" with this command: npx skills add jstarfilms/vibecode-protocol-suite/jstarfilms-vibecode-protocol-suite-convex-realtime

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:

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&#x3C;"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" &#x26;&#x26; (
    &#x3C;button onClick={() => loadMore(20)}>Load More&#x3C;/button>
  )}
  
  {status === "LoadingMore" &#x26;&#x26; &#x3C;div>Loading...&#x3C;/div>}
  
  {status === "Exhausted" &#x26;&#x26; &#x3C;div>No more messages&#x3C;/div>}
&#x3C;/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 &#x26;&#x26; 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"; 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<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>

  &#x3C;form onSubmit={handleSend}>
    &#x3C;input
      value={input}
      onChange={(e) => setInput(e.target.value)}
      placeholder="Type a message..."
    />
    &#x3C;button type="submit">Send&#x3C;/button>
  &#x3C;/form>
&#x3C;/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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

youtube-pipeline

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

google-trends

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

nextjs-standards

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ui-ux-pro-max

No summary provided by upstream source.

Repository SourceNeeds Review