[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CCD879QGRDF2B5WNS4WDQF":3,"skill-same:01M6CCD879QGRDF2B5WNS4WDQF":60},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":30,"linkOnly":31,"markdownContent":32,"markdownBody":33,"frontMatter":34,"refName":39,"commitSha":40,"skillDirectory":41,"forksCount":14,"repositoryHtmlUrl":42,"repositoryHomepageUrl":43,"ownerHtmlUrl":44,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":45,"install":46,"resources":59},"01M6CCD879QGRDF2B5WNS4WDQF","convex-realtime",null,"Patterns for building reactive apps including subscription management, optimistic updates, cache behavior, and paginated queries with cursor-based loading","baisharg\u002FASTN","baisharg","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F204916775?v=4","01M6CC8XHZYVK2DR22FZ1BESCG","BAISH","01M6CC8XP400QZ9DXAWTC8EG3X",2,"AGPL-3.0","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fconvex-realtime\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29],"convex","realtime","convex realtime","cache","behavior","management","cursor","queries","subscriptions","optimistic-updates","pagination","2026-11-10 10:12:30",false,"---\nname: convex-realtime\ndisplayName: Convex Realtime\ndescription: Patterns for building reactive apps including subscription management, optimistic updates, cache behavior, and paginated queries with cursor-based loading\nversion: 1.0.0\nauthor: Convex\ntags: [convex, realtime, subscriptions, optimistic-updates, pagination]\n---\n\n# Convex Realtime\n\nBuild reactive applications with Convex's real-time subscriptions, optimistic updates, intelligent caching, and cursor-based pagination.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https:\u002F\u002Fdocs.convex.dev\u002Fclient\u002Freact\n- Optimistic Updates: https:\u002F\u002Fdocs.convex.dev\u002Fclient\u002Freact\u002Foptimistic-updates\n- Pagination: https:\u002F\u002Fdocs.convex.dev\u002Fdatabase\u002Fpagination\n- For broader context: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n\n## Instructions\n\n### How Convex Realtime Works\n\n1. **Automatic Subscriptions** - useQuery creates a subscription that updates automatically\n2. **Smart Caching** - Query results are cached and shared across components\n3. **Consistency** - All subscriptions see a consistent view of the database\n4. **Efficient Updates** - Only re-renders when relevant data changes\n\n### Basic Subscriptions\n\n```typescript\n\u002F\u002F React component with real-time data\nimport { useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction TaskList({ userId }: { userId: Id\u003C\"users\"> }) {\n  \u002F\u002F Automatically subscribes and updates in real-time\n  const tasks = useQuery(api.tasks.list, { userId });\n\n  if (tasks === undefined) {\n    return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  }\n\n  return (\n    \u003Cul>\n      {tasks.map((task) => (\n        \u003Cli key={task._id}>{task.title}\u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  );\n}\n```\n\n### Conditional Queries\n\n```typescript\nimport { useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction UserProfile({ userId }: { userId: Id\u003C\"users\"> | null }) {\n  \u002F\u002F Skip query when userId is null\n  const user = useQuery(\n    api.users.get,\n    userId ? { userId } : \"skip\"\n  );\n\n  if (userId === null) {\n    return \u003Cdiv>Select a user\u003C\u002Fdiv>;\n  }\n\n  if (user === undefined) {\n    return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  }\n\n  return \u003Cdiv>{user.name}\u003C\u002Fdiv>;\n}\n```\n\n### Mutations with Real-time Updates\n\n```typescript\nimport { useMutation, useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction TaskManager({ userId }: { userId: Id\u003C\"users\"> }) {\n  const tasks = useQuery(api.tasks.list, { userId });\n  const createTask = useMutation(api.tasks.create);\n  const toggleTask = useMutation(api.tasks.toggle);\n\n  const handleCreate = async (title: string) => {\n    \u002F\u002F Mutation triggers automatic re-render when data changes\n    await createTask({ title, userId });\n  };\n\n  const handleToggle = async (taskId: Id\u003C\"tasks\">) => {\n    await toggleTask({ taskId });\n  };\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => handleCreate(\"New Task\")}>Add Task\u003C\u002Fbutton>\n      \u003Cul>\n        {tasks?.map((task) => (\n          \u003Cli key={task._id} onClick={() => handleToggle(task._id)}>\n            {task.completed ? \"✓\" : \"○\"} {task.title}\n          \u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Optimistic Updates\n\nShow changes immediately before server confirmation:\n\n```typescript\nimport { useMutation, useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { Id } from \"..\u002Fconvex\u002F_generated\u002FdataModel\";\n\nfunction TaskItem({ task }: { task: Task }) {\n  const toggleTask = useMutation(api.tasks.toggle).withOptimisticUpdate(\n    (localStore, args) => {\n      const { taskId } = args;\n      const currentValue = localStore.getQuery(api.tasks.get, { taskId });\n\n      if (currentValue !== undefined) {\n        localStore.setQuery(api.tasks.get, { taskId }, {\n          ...currentValue,\n          completed: !currentValue.completed,\n        });\n      }\n    }\n  );\n\n  return (\n    \u003Cdiv onClick={() => toggleTask({ taskId: task._id })}>\n      {task.completed ? \"✓\" : \"○\"} {task.title}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Optimistic Updates for Lists\n\n```typescript\nimport { useMutation } from 'convex\u002Freact'\nimport { api } from '..\u002Fconvex\u002F_generated\u002Fapi'\n\nfunction useCreateTask(userId: Id\u003C'users'>) {\n  return useMutation(api.tasks.create).withOptimisticUpdate(\n    (localStore, args) => {\n      const { title, userId } = args\n      const currentTasks = localStore.getQuery(api.tasks.list, { userId })\n\n      if (currentTasks !== undefined) {\n        \u002F\u002F Add optimistic task to the list\n        const optimisticTask = {\n          _id: crypto.randomUUID() as Id\u003C'tasks'>,\n          _creationTime: Date.now(),\n          title,\n          userId,\n          completed: false,\n        }\n\n        localStore.setQuery(api.tasks.list, { userId }, [\n          optimisticTask,\n          ...currentTasks,\n        ])\n      }\n    },\n  )\n}\n```\n\n### Cursor-Based Pagination\n\n```typescript\n\u002F\u002F convex\u002Fmessages.ts\nimport { query } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\nimport { paginationOptsValidator } from 'convex\u002Fserver'\n\nexport const listPaginated = query({\n  args: {\n    channelId: v.id('channels'),\n    paginationOpts: paginationOptsValidator,\n  },\n  handler: async (ctx, args) => {\n    return await ctx.db\n      .query('messages')\n      .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))\n      .order('desc')\n      .paginate(args.paginationOpts)\n  },\n})\n```\n\n```typescript\n\u002F\u002F React component with pagination\nimport { usePaginatedQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction MessageList({ channelId }: { channelId: Id\u003C\"channels\"> }) {\n  const { results, status, loadMore } = usePaginatedQuery(\n    api.messages.listPaginated,\n    { channelId },\n    { initialNumItems: 20 }\n  );\n\n  return (\n    \u003Cdiv>\n      {results.map((message) => (\n        \u003Cdiv key={message._id}>{message.content}\u003C\u002Fdiv>\n      ))}\n\n      {status === \"CanLoadMore\" && (\n        \u003Cbutton onClick={() => loadMore(20)}>Load More\u003C\u002Fbutton>\n      )}\n\n      {status === \"LoadingMore\" && \u003Cdiv>Loading...\u003C\u002Fdiv>}\n\n      {status === \"Exhausted\" && \u003Cdiv>No more messages\u003C\u002Fdiv>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Infinite Scroll Pattern\n\n```typescript\nimport { usePaginatedQuery } from \"convex\u002Freact\";\nimport { useEffect, useRef } from \"react\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction InfiniteMessageList({ channelId }: { channelId: Id\u003C\"channels\"> }) {\n  const { results, status, loadMore } = usePaginatedQuery(\n    api.messages.listPaginated,\n    { channelId },\n    { initialNumItems: 20 }\n  );\n\n  const observerRef = useRef\u003CIntersectionObserver>();\n  const loadMoreRef = useRef\u003CHTMLDivElement>(null);\n\n  useEffect(() => {\n    if (observerRef.current) {\n      observerRef.current.disconnect();\n    }\n\n    observerRef.current = new IntersectionObserver((entries) => {\n      if (entries[0].isIntersecting && status === \"CanLoadMore\") {\n        loadMore(20);\n      }\n    });\n\n    if (loadMoreRef.current) {\n      observerRef.current.observe(loadMoreRef.current);\n    }\n\n    return () => observerRef.current?.disconnect();\n  }, [status, loadMore]);\n\n  return (\n    \u003Cdiv>\n      {results.map((message) => (\n        \u003Cdiv key={message._id}>{message.content}\u003C\u002Fdiv>\n      ))}\n      \u003Cdiv ref={loadMoreRef} style={{ height: 1 }} \u002F>\n      {status === \"LoadingMore\" && \u003Cdiv>Loading...\u003C\u002Fdiv>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Multiple Subscriptions\n\n```typescript\nimport { useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction Dashboard({ userId }: { userId: Id\u003C\"users\"> }) {\n  \u002F\u002F Multiple subscriptions update independently\n  const user = useQuery(api.users.get, { userId });\n  const tasks = useQuery(api.tasks.list, { userId });\n  const notifications = useQuery(api.notifications.unread, { userId });\n\n  const isLoading = user === undefined ||\n                    tasks === undefined ||\n                    notifications === undefined;\n\n  if (isLoading) {\n    return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  }\n\n  return (\n    \u003Cdiv>\n      \u003Ch1>Welcome, {user.name}\u003C\u002Fh1>\n      \u003Cp>You have {tasks.length} tasks\u003C\u002Fp>\n      \u003Cp>{notifications.length} unread notifications\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## Examples\n\n### Real-time Chat Application\n\n```typescript\n\u002F\u002F convex\u002Fmessages.ts\nimport { query, mutation } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport const list = query({\n  args: { channelId: v.id('channels') },\n  returns: v.array(\n    v.object({\n      _id: v.id('messages'),\n      _creationTime: v.number(),\n      content: v.string(),\n      authorId: v.id('users'),\n      authorName: v.string(),\n    }),\n  ),\n  handler: async (ctx, args) => {\n    const messages = await ctx.db\n      .query('messages')\n      .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))\n      .order('desc')\n      .take(100)\n\n    \u002F\u002F Enrich with author names\n    return Promise.all(\n      messages.map(async (msg) => {\n        const author = await ctx.db.get(msg.authorId)\n        return {\n          ...msg,\n          authorName: author?.name ?? 'Unknown',\n        }\n      }),\n    )\n  },\n})\n\nexport const send = mutation({\n  args: {\n    channelId: v.id('channels'),\n    authorId: v.id('users'),\n    content: v.string(),\n  },\n  returns: v.id('messages'),\n  handler: async (ctx, args) => {\n    return await ctx.db.insert('messages', {\n      channelId: args.channelId,\n      authorId: args.authorId,\n      content: args.content,\n    })\n  },\n})\n```\n\n```typescript\n\u002F\u002F ChatRoom.tsx\nimport { useQuery, useMutation } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { useState, useRef, useEffect } from \"react\";\n\nfunction ChatRoom({ channelId, userId }: Props) {\n  const messages = useQuery(api.messages.list, { channelId });\n  const sendMessage = useMutation(api.messages.send);\n  const [input, setInput] = useState(\"\");\n  const messagesEndRef = useRef\u003CHTMLDivElement>(null);\n\n  \u002F\u002F Auto-scroll to bottom on new messages\n  useEffect(() => {\n    messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n  }, [messages]);\n\n  const handleSend = async (e: React.FormEvent) => {\n    e.preventDefault();\n    if (!input.trim()) return;\n\n    await sendMessage({\n      channelId,\n      authorId: userId,\n      content: input.trim(),\n    });\n    setInput(\"\");\n  };\n\n  return (\n    \u003Cdiv className=\"chat-room\">\n      \u003Cdiv className=\"messages\">\n        {messages?.map((msg) => (\n          \u003Cdiv key={msg._id} className=\"message\">\n            \u003Cstrong>{msg.authorName}:\u003C\u002Fstrong> {msg.content}\n          \u003C\u002Fdiv>\n        ))}\n        \u003Cdiv ref={messagesEndRef} \u002F>\n      \u003C\u002Fdiv>\n\n      \u003Cform onSubmit={handleSend}>\n        \u003Cinput\n          value={input}\n          onChange={(e) => setInput(e.target.value)}\n          placeholder=\"Type a message...\"\n        \u002F>\n        \u003Cbutton type=\"submit\">Send\u003C\u002Fbutton>\n      \u003C\u002Fform>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Use \"skip\" for conditional queries instead of conditionally calling hooks\n- Implement optimistic updates for better perceived performance\n- Use usePaginatedQuery for large datasets\n- Handle undefined state (loading) explicitly\n- Avoid unnecessary re-renders by memoizing derived data\n\n## Common Pitfalls\n\n1. **Conditional hook calls** - Use \"skip\" instead of if statements\n2. **Not handling loading state** - Always check for undefined\n3. **Missing optimistic update rollback** - Optimistic updates auto-rollback on error\n4. **Over-fetching with pagination** - Use appropriate page sizes\n5. **Ignoring subscription cleanup** - React handles this automatically\n\n## References\n\n- Convex Documentation: https:\u002F\u002Fdocs.convex.dev\u002F\n- Convex LLMs.txt: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n- React Client: https:\u002F\u002Fdocs.convex.dev\u002Fclient\u002Freact\n- Optimistic Updates: https:\u002F\u002Fdocs.convex.dev\u002Fclient\u002Freact\u002Foptimistic-updates\n- Pagination: https:\u002F\u002Fdocs.convex.dev\u002Fdatabase\u002Fpagination\n","# Convex Realtime\n\nBuild reactive applications with Convex's real-time subscriptions, optimistic updates, intelligent caching, and cursor-based pagination.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https:\u002F\u002Fdocs.convex.dev\u002Fclient\u002Freact\n- Optimistic Updates: https:\u002F\u002Fdocs.convex.dev\u002Fclient\u002Freact\u002Foptimistic-updates\n- Pagination: https:\u002F\u002Fdocs.convex.dev\u002Fdatabase\u002Fpagination\n- For broader context: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n\n## Instructions\n\n### How Convex Realtime Works\n\n1. **Automatic Subscriptions** - useQuery creates a subscription that updates automatically\n2. **Smart Caching** - Query results are cached and shared across components\n3. **Consistency** - All subscriptions see a consistent view of the database\n4. **Efficient Updates** - Only re-renders when relevant data changes\n\n### Basic Subscriptions\n\n```typescript\n\u002F\u002F React component with real-time data\nimport { useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction TaskList({ userId }: { userId: Id\u003C\"users\"> }) {\n  \u002F\u002F Automatically subscribes and updates in real-time\n  const tasks = useQuery(api.tasks.list, { userId });\n\n  if (tasks === undefined) {\n    return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  }\n\n  return (\n    \u003Cul>\n      {tasks.map((task) => (\n        \u003Cli key={task._id}>{task.title}\u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  );\n}\n```\n\n### Conditional Queries\n\n```typescript\nimport { useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction UserProfile({ userId }: { userId: Id\u003C\"users\"> | null }) {\n  \u002F\u002F Skip query when userId is null\n  const user = useQuery(\n    api.users.get,\n    userId ? { userId } : \"skip\"\n  );\n\n  if (userId === null) {\n    return \u003Cdiv>Select a user\u003C\u002Fdiv>;\n  }\n\n  if (user === undefined) {\n    return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  }\n\n  return \u003Cdiv>{user.name}\u003C\u002Fdiv>;\n}\n```\n\n### Mutations with Real-time Updates\n\n```typescript\nimport { useMutation, useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction TaskManager({ userId }: { userId: Id\u003C\"users\"> }) {\n  const tasks = useQuery(api.tasks.list, { userId });\n  const createTask = useMutation(api.tasks.create);\n  const toggleTask = useMutation(api.tasks.toggle);\n\n  const handleCreate = async (title: string) => {\n    \u002F\u002F Mutation triggers automatic re-render when data changes\n    await createTask({ title, userId });\n  };\n\n  const handleToggle = async (taskId: Id\u003C\"tasks\">) => {\n    await toggleTask({ taskId });\n  };\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => handleCreate(\"New Task\")}>Add Task\u003C\u002Fbutton>\n      \u003Cul>\n        {tasks?.map((task) => (\n          \u003Cli key={task._id} onClick={() => handleToggle(task._id)}>\n            {task.completed ? \"✓\" : \"○\"} {task.title}\n          \u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Optimistic Updates\n\nShow changes immediately before server confirmation:\n\n```typescript\nimport { useMutation, useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { Id } from \"..\u002Fconvex\u002F_generated\u002FdataModel\";\n\nfunction TaskItem({ task }: { task: Task }) {\n  const toggleTask = useMutation(api.tasks.toggle).withOptimisticUpdate(\n    (localStore, args) => {\n      const { taskId } = args;\n      const currentValue = localStore.getQuery(api.tasks.get, { taskId });\n\n      if (currentValue !== undefined) {\n        localStore.setQuery(api.tasks.get, { taskId }, {\n          ...currentValue,\n          completed: !currentValue.completed,\n        });\n      }\n    }\n  );\n\n  return (\n    \u003Cdiv onClick={() => toggleTask({ taskId: task._id })}>\n      {task.completed ? \"✓\" : \"○\"} {task.title}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Optimistic Updates for Lists\n\n```typescript\nimport { useMutation } from 'convex\u002Freact'\nimport { api } from '..\u002Fconvex\u002F_generated\u002Fapi'\n\nfunction useCreateTask(userId: Id\u003C'users'>) {\n  return useMutation(api.tasks.create).withOptimisticUpdate(\n    (localStore, args) => {\n      const { title, userId } = args\n      const currentTasks = localStore.getQuery(api.tasks.list, { userId })\n\n      if (currentTasks !== undefined) {\n        \u002F\u002F Add optimistic task to the list\n        const optimisticTask = {\n          _id: crypto.randomUUID() as Id\u003C'tasks'>,\n          _creationTime: Date.now(),\n          title,\n          userId,\n          completed: false,\n        }\n\n        localStore.setQuery(api.tasks.list, { userId }, [\n          optimisticTask,\n          ...currentTasks,\n        ])\n      }\n    },\n  )\n}\n```\n\n### Cursor-Based Pagination\n\n```typescript\n\u002F\u002F convex\u002Fmessages.ts\nimport { query } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\nimport { paginationOptsValidator } from 'convex\u002Fserver'\n\nexport const listPaginated = query({\n  args: {\n    channelId: v.id('channels'),\n    paginationOpts: paginationOptsValidator,\n  },\n  handler: async (ctx, args) => {\n    return await ctx.db\n      .query('messages')\n      .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))\n      .order('desc')\n      .paginate(args.paginationOpts)\n  },\n})\n```\n\n```typescript\n\u002F\u002F React component with pagination\nimport { usePaginatedQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction MessageList({ channelId }: { channelId: Id\u003C\"channels\"> }) {\n  const { results, status, loadMore } = usePaginatedQuery(\n    api.messages.listPaginated,\n    { channelId },\n    { initialNumItems: 20 }\n  );\n\n  return (\n    \u003Cdiv>\n      {results.map((message) => (\n        \u003Cdiv key={message._id}>{message.content}\u003C\u002Fdiv>\n      ))}\n\n      {status === \"CanLoadMore\" && (\n        \u003Cbutton onClick={() => loadMore(20)}>Load More\u003C\u002Fbutton>\n      )}\n\n      {status === \"LoadingMore\" && \u003Cdiv>Loading...\u003C\u002Fdiv>}\n\n      {status === \"Exhausted\" && \u003Cdiv>No more messages\u003C\u002Fdiv>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Infinite Scroll Pattern\n\n```typescript\nimport { usePaginatedQuery } from \"convex\u002Freact\";\nimport { useEffect, useRef } from \"react\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction InfiniteMessageList({ channelId }: { channelId: Id\u003C\"channels\"> }) {\n  const { results, status, loadMore } = usePaginatedQuery(\n    api.messages.listPaginated,\n    { channelId },\n    { initialNumItems: 20 }\n  );\n\n  const observerRef = useRef\u003CIntersectionObserver>();\n  const loadMoreRef = useRef\u003CHTMLDivElement>(null);\n\n  useEffect(() => {\n    if (observerRef.current) {\n      observerRef.current.disconnect();\n    }\n\n    observerRef.current = new IntersectionObserver((entries) => {\n      if (entries[0].isIntersecting && status === \"CanLoadMore\") {\n        loadMore(20);\n      }\n    });\n\n    if (loadMoreRef.current) {\n      observerRef.current.observe(loadMoreRef.current);\n    }\n\n    return () => observerRef.current?.disconnect();\n  }, [status, loadMore]);\n\n  return (\n    \u003Cdiv>\n      {results.map((message) => (\n        \u003Cdiv key={message._id}>{message.content}\u003C\u002Fdiv>\n      ))}\n      \u003Cdiv ref={loadMoreRef} style={{ height: 1 }} \u002F>\n      {status === \"LoadingMore\" && \u003Cdiv>Loading...\u003C\u002Fdiv>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Multiple Subscriptions\n\n```typescript\nimport { useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction Dashboard({ userId }: { userId: Id\u003C\"users\"> }) {\n  \u002F\u002F Multiple subscriptions update independently\n  const user = useQuery(api.users.get, { userId });\n  const tasks = useQuery(api.tasks.list, { userId });\n  const notifications = useQuery(api.notifications.unread, { userId });\n\n  const isLoading = user === undefined ||\n                    tasks === undefined ||\n                    notifications === undefined;\n\n  if (isLoading) {\n    return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  }\n\n  return (\n    \u003Cdiv>\n      \u003Ch1>Welcome, {user.name}\u003C\u002Fh1>\n      \u003Cp>You have {tasks.length} tasks\u003C\u002Fp>\n      \u003Cp>{notifications.length} unread notifications\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## Examples\n\n### Real-time Chat Application\n\n```typescript\n\u002F\u002F convex\u002Fmessages.ts\nimport { query, mutation } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport const list = query({\n  args: { channelId: v.id('channels') },\n  returns: v.array(\n    v.object({\n      _id: v.id('messages'),\n      _creationTime: v.number(),\n      content: v.string(),\n      authorId: v.id('users'),\n      authorName: v.string(),\n    }),\n  ),\n  handler: async (ctx, args) => {\n    const messages = await ctx.db\n      .query('messages')\n      .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))\n      .order('desc')\n      .take(100)\n\n    \u002F\u002F Enrich with author names\n    return Promise.all(\n      messages.map(async (msg) => {\n        const author = await ctx.db.get(msg.authorId)\n        return {\n          ...msg,\n          authorName: author?.name ?? 'Unknown',\n        }\n      }),\n    )\n  },\n})\n\nexport const send = mutation({\n  args: {\n    channelId: v.id('channels'),\n    authorId: v.id('users'),\n    content: v.string(),\n  },\n  returns: v.id('messages'),\n  handler: async (ctx, args) => {\n    return await ctx.db.insert('messages', {\n      channelId: args.channelId,\n      authorId: args.authorId,\n      content: args.content,\n    })\n  },\n})\n```\n\n```typescript\n\u002F\u002F ChatRoom.tsx\nimport { useQuery, useMutation } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { useState, useRef, useEffect } from \"react\";\n\nfunction ChatRoom({ channelId, userId }: Props) {\n  const messages = useQuery(api.messages.list, { channelId });\n  const sendMessage = useMutation(api.messages.send);\n  const [input, setInput] = useState(\"\");\n  const messagesEndRef = useRef\u003CHTMLDivElement>(null);\n\n  \u002F\u002F Auto-scroll to bottom on new messages\n  useEffect(() => {\n    messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n  }, [messages]);\n\n  const handleSend = async (e: React.FormEvent) => {\n    e.preventDefault();\n    if (!input.trim()) return;\n\n    await sendMessage({\n      channelId,\n      authorId: userId,\n      content: input.trim(),\n    });\n    setInput(\"\");\n  };\n\n  return (\n    \u003Cdiv className=\"chat-room\">\n      \u003Cdiv className=\"messages\">\n        {messages?.map((msg) => (\n          \u003Cdiv key={msg._id} className=\"message\">\n            \u003Cstrong>{msg.authorName}:\u003C\u002Fstrong> {msg.content}\n          \u003C\u002Fdiv>\n        ))}\n        \u003Cdiv ref={messagesEndRef} \u002F>\n      \u003C\u002Fdiv>\n\n      \u003Cform onSubmit={handleSend}>\n        \u003Cinput\n          value={input}\n          onChange={(e) => setInput(e.target.value)}\n          placeholder=\"Type a message...\"\n        \u002F>\n        \u003Cbutton type=\"submit\">Send\u003C\u002Fbutton>\n      \u003C\u002Fform>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Use \"skip\" for conditional queries instead of conditionally calling hooks\n- Implement optimistic updates for better perceived performance\n- Use usePaginatedQuery for large datasets\n- Handle undefined state (loading) explicitly\n- Avoid unnecessary re-renders by memoizing derived data\n\n## Common Pitfalls\n\n1. **Conditional hook calls** - Use \"skip\" instead of if statements\n2. **Not handling loading state** - Always check for undefined\n3. **Missing optimistic update rollback** - Optimistic updates auto-rollback on error\n4. **Over-fetching with pagination** - Use appropriate page sizes\n5. **Ignoring subscription cleanup** - React handles this automatically\n\n## References\n\n- Convex Documentation: https:\u002F\u002Fdocs.convex.dev\u002F\n- Convex LLMs.txt: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n- React Client: https:\u002F\u002Fdocs.convex.dev\u002Fclient\u002Freact\n- Optimistic Updates: https:\u002F\u002Fdocs.convex.dev\u002Fclient\u002Freact\u002Foptimistic-updates\n- Pagination: https:\u002F\u002Fdocs.convex.dev\u002Fdatabase\u002Fpagination\n",{"name":5,"tags":35,"author":36,"version":37,"description":7,"displayName":38},[19,20,27,28,29],"Convex","1.0.0","Convex Realtime","main","83212e6d909b6a0fe9e93fd5a3ff1723300f5b5b",".claude\u002Fskills\u002Fconvex-realtime","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN","https:\u002F\u002Fsafetytalent.org","https:\u002F\u002Fgithub.com\u002Fbaisharg","2026-11-04 07:01:33",{"source":8,"skillName":5,"commands":47,"prompt":57,"zipAvailable":58},[48,51,54],{"tool":49,"command":50},"npx","npx skills add baisharg\u002FASTN --skill convex-realtime",{"tool":52,"command":53},"bunx","bunx skills add baisharg\u002FASTN --skill convex-realtime",{"tool":55,"command":56},"pnpm","pnpm dlx skills add baisharg\u002FASTN --skill convex-realtime","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fconvex-realtime\u002FSKILL.md",true,[],[61,83,97,111,125,142],{"skillNo":62,"name":63,"nameZh":6,"description":64,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":65,"categories":66,"keywords":72,"publishedAt":30},"01M6CCE5DVER8DQWJDY51HAJF7","schema-builder","Design and generate Convex database schemas with proper validation, indexes, and relationships. Use when creating schema.ts or modifying table definitions.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fschema-builder\u002FSKILL.md",[67],{"categoryNo":68,"categoryName":69,"taxonomyType":70,"taxonomyTypeDesc":71},"01JBSEEDCAT000000000000015","视觉设计","SCENARIO","使用场景",[73,74,75,76,77,78,79,80,81,19,82],"schema builder","schema","builder","indexes","validation","use","table","database","design","generate",{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":30},"01M6CCE36AA57CG36R4VHKFRXV","integration-tanstack-start","PostHog integration for TanStack Start full-stack applications","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fposthog-integration-tanstack-start\u002FSKILL.md",[],[90,91,92,93,94,95,96],"integration","start","tanstack","integration tanstack start","posthog","applications","stack",{"skillNo":98,"name":99,"nameZh":6,"description":100,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":101,"categories":102,"keywords":103,"publishedAt":30},"01M6CCE0FK6N8XAB093X7TTQ4F","migration-helper","Plan and execute Convex schema migrations safely, including adding fields, creating tables, and data transformations. Use when schema changes affect existing data.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fmigration-helper\u002FSKILL.md",[],[104,105,106,107,108,109,78,74,110,19],"migration","migration helper","schema migrations safely","data","plan","execute","migrations",{"skillNo":112,"name":113,"nameZh":6,"description":114,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":115,"categories":116,"keywords":117,"publishedAt":30},"01M6CCDY3644G8WXXP0BDNMZPR","gsd:sprint","Autonomous phase execution with Codex validation. Runs plan\u002Fexecute\u002Fverify cycles in a bash loop until done or halted.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgsd-sprint\u002FSKILL.md",[],[113,118,119,77,91,120,121,122,123,108,124,109],"resume","codex","execution","verify","Bash","loop","autonomous",{"skillNo":126,"name":127,"nameZh":6,"description":128,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":131,"publishedAt":30},"01M6CCDVTZRTFWWGREMV6FF956","gsd:second-opinion","Get a second opinion from OpenAI Codex (GPT-5.2-codex) on code, architecture, or implementation decisions. Use when you want independent verification or an alternative perspective.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgsd-second-opinion\u002FSKILL.md",[],[132,133,134,119,135,136,137,138,139,140,78,141],"gsd:second opinion","second opinion","question","architecture","code","verification","openai","gpt","decisions","implementation",{"skillNo":143,"name":144,"nameZh":6,"description":145,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":146,"categories":147,"keywords":148,"publishedAt":30},"01M6CCDSDPYYNEDERHP01N30PT","gsd:milestone-sprint","Run entire milestone autonomously with Codex validation. Auto-detects current milestone, executes all phases, and runs audit.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgsd-milestone-sprint\u002FSKILL.md",[],[149,150,77,151,152,153,118,119],"gsd:milestone sprint","run","audit","interactive","auto"]