Skip to main content

convex-realtime

Patterns for building reactive apps including subscription management, optimistic updates, cache behavior, and paginated queries with cursor-based loading

الانتقال إلى التثبيت

معلومات المصدر

المستودع
mediar-ai/skillhubz
آخر نشاط في المصدر
١ مارس ٢٠٢٦ في ٢٢:٥٨
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٧
التفرعات
٤

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
convex-realtime
displayName
Convex Realtime
description
Patterns for building reactive apps including subscription management, optimistic updates, cache behavior, and paginated queries with cursor-based loading
version
1.0.0
author
Convex
tags
["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 automatically 2. **Smart Caching** - Query results are cached and shared across components 3. **Consistency** - All subscriptions see a consistent view of the database 4. **Efficient Updates** - Only re-renders when relevant data changes ### Basic Subscriptions ```typescript // 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 ```typescript 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 ```typescript 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: ```typescript 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 ```typescript 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 ```typescript // 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); }, }); ``` ```typescript // 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 ```typescript 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 ```typescript 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 ```typescript // 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, }); }, }); ``` ```typescript // 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();
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub