convex-react

convex-react

Convex React client - hooks, real-time updates, optimistic updates, pagination, and UI patterns. Use when working with useQuery, useMutation, useAction, usePaginatedQuery, convex/react, ConvexProvider, ConvexReactClient, optimistic updates, skip, real-time, or loading states in React.

0étoiles
0forks
Mis à jour 1/17/2026
SKILL.md
readonlyread-only
name
convex-react
description

Convex React client - hooks, real-time updates, optimistic updates, pagination, and UI patterns. Use when working with useQuery, useMutation, useAction, usePaginatedQuery, convex/react, ConvexProvider, ConvexReactClient, optimistic updates, skip, real-time, or loading states in React.

version
"1.0"

Convex React Client Guide

Complete React client guidelines for Convex, including hooks, real-time updates, optimistic updates, and best practices for building reactive UIs.


Basic React Integration

Complete Example

import React, { useState } from "react";
import { useMutation, useQuery } from "convex/react";
import { api } from "../convex/_generated/api";

export default function App() {
  const messages = useQuery(api.messages.list) || [];

  const [newMessageText, setNewMessageText] = useState("");
  const sendMessage = useMutation(api.messages.send);

  const [name] = useState(() => "User " + Math.floor(Math.random() * 10000));

  async function handleSendMessage(event: React.FormEvent) {
    event.preventDefault();
    await sendMessage({ body: newMessageText, author: name });
    setNewMessageText("");
  }

  return (
    <main>
      <h1>Convex Chat</h1>
      <p className="badge">
        <span>{name}</span>
      </p>
      <ul>
        {messages.map((message) => (
          <li key={message._id}>
            <span>{message.author}:</span>
            <span>{message.body}</span>
            <span>{new Date(message._creationTime).toLocaleTimeString()}</span>
          </li>
        ))}
      </ul>
      <form onSubmit={handleSendMessage}>
        <input
          value={newMessageText}
          onChange={(event) => setNewMessageText(event.target.value)}
          placeholder="Write a message..."
        />
        <button type="submit" disabled={!newMessageText}>
          Send
        </button>
      </form>
    </main>
  );
}

useQuery Hook

Real-time Updates

The useQuery() hook is live-updating! It causes the React component to rerender automatically when data changes. Convex is a perfect fit for collaborative, live-updating websites.

Return Values

  • undefined - Query is loading
  • null - Query returned null (e.g., user not found)
  • data - Query returned data
function UserProfile({ userId }: { userId: Id<"users"> }) {
  const user = useQuery(api.users.get, { userId });

  // Loading state
  if (user === undefined) {
    return <div>Loading...</div>;
  }

  // Not found
  if (user === null) {
    return <div>User not found</div>;
  }

  // Data loaded
  return <div>{user.name}</div>;
}

Conditional Queries with "skip"

CRITICAL: Never Use Hooks Conditionally

// WRONG - Will cause React hook errors!
const avatarUrl = profile?.avatarId
  ? useQuery(api.profiles.getAvatarUrl, { storageId: profile.avatarId })
  : null;

// CORRECT - Use "skip" to conditionally skip the query
const avatarUrl = useQuery(
  api.profiles.getAvatarUrl,
  profile?.avatarId ? { storageId: profile.avatarId } : "skip"
);

More Examples

function Dashboard() {
  const user = useQuery(api.auth.loggedInUser);

  // Skip queries until we have user data
  const userPosts = useQuery(
    api.posts.getByUser,
    user ? { userId: user._id } : "skip"
  );

  const userSettings = useQuery(
    api.settings.get,
    user ? { userId: user._id } : "skip"
  );

  if (user === undefined) {
    return <Loading />;
  }

  if (user === null) {
    return <LoginPrompt />;
  }

  return (
    <div>
      <PostList posts={userPosts || []} />
      <Settings settings={userSettings} />
    </div>
  );
}

useMutation Hook

Basic Usage

function CreatePost() {
  const createPost = useMutation(api.posts.create);
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsSubmitting(true);

    try {
      await createPost({ title, content });
      setTitle("");
      setContent("");
    } catch (error) {
      console.error("Failed to create post:", error);
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Title"
        disabled={isSubmitting}
      />
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="Content"
        disabled={isSubmitting}
      />
      <button type="submit" disabled={isSubmitting || !title || !content}>
        {isSubmitting ? "Creating..." : "Create Post"}
      </button>
    </form>
  );
}

useAction Hook

import { useAction } from "convex/react";
import { api } from "../convex/_generated/api";

function AIChat() {
  const generateResponse = useAction(api.ai.generateResponse);
  const [prompt, setPrompt] = useState("");
  const [response, setResponse] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsLoading(true);

    try {
      const result = await generateResponse({ prompt });
      setResponse(result);
    } catch (error) {
      console.error("AI generation failed:", error);
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="Ask AI..."
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading || !prompt}>
          {isLoading ? "Thinking..." : "Ask"}
        </button>
      </form>
      {response && <p>{response}</p>}
    </div>
  );
}

Importing the API Object

When writing a UI component and you want to use a Convex function, you MUST import the api object:

import { api } from "../convex/_generated/api";

You can use the api object to call any public Convex function.

Always make sure:

  1. The functions you are calling are defined in the convex/ directory
  2. Use the api object for public functions
  3. You are using the correct arguments for convex functions
  4. If arguments are not optional, make sure they are not null

Pagination with usePaginatedQuery

import { usePaginatedQuery } from "convex/react";
import { api } from "../convex/_generated/api";

function InfiniteMessageList({ channelId }: { channelId: Id<"channels"> }) {
  const { results, status, loadMore } = usePaginatedQuery(
    api.messages.list,
    { 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>
  );
}

Optimistic Updates

import { useMutation, useQuery } from "convex/react";
import { api } from "../convex/_generated/api";

function TodoList() {
  const todos = useQuery(api.todos.list) || [];
  const toggleTodo = useMutation(api.todos.toggle).withOptimisticUpdate(
    (localStore, args) => {
      const currentTodos = localStore.getQuery(api.todos.list);
      if (currentTodos !== undefined) {
        const updatedTodos = currentTodos.map((todo) =>
          todo._id === args.id
            ? { ...todo, completed: !todo.completed }
            : todo
        );
        localStore.setQuery(api.todos.list, {}, updatedTodos);
      }
    }
  );

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo._id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo({ id: todo._id })}
          />
          {todo.title}
        </li>
      ))}
    </ul>
  );
}

Error Handling

function PostForm() {
  const createPost = useMutation(api.posts.create);
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(data: FormData) {
    setError(null);

    try {
      await createPost({
        title: data.get("title") as string,
        content: data.get("content") as string,
      });
    } catch (e) {
      if (e instanceof Error) {
        setError(e.message);
      } else {
        setError("An unexpected error occurred");
      }
    }
  }

  return (
    <form action={handleSubmit}>
      {error && <div className="error">{error}</div>}
      {/* form fields */}
    </form>
  );
}

Loading States Pattern

function DataComponent() {
  const data = useQuery(api.data.get);

  // Pattern 1: Simple loading check
  if (data === undefined) {
    return <Skeleton />;
  }

  // Pattern 2: With null check
  if (data === null) {
    return <NotFound />;
  }

  return <DataView data={data} />;
}

File Upload Pattern

function ImageUploader() {
  const generateUploadUrl = useMutation(api.files.generateUploadUrl);
  const saveFile = useMutation(api.files.save);
  const [uploading, setUploading] = useState(false);

  async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;

    setUploading(true);

    try {
      // Step 1: Get upload URL
      const uploadUrl = await generateUploadUrl();

      // Step 2: Upload file
      const result = await fetch(uploadUrl, {
        method: "POST",
        headers: { "Content-Type": file.type },
        body: file,
      });

      if (!result.ok) {
        throw new Error("Upload failed");
      }

      const { storageId } = await result.json();

      // Step 3: Save reference to database
      await saveFile({ storageId, fileName: file.name });

    } catch (error) {
      console.error("Upload error:", error);
    } finally {
      setUploading(false);
    }
  }

  return (
    <input
      type="file"
      onChange={handleFileChange}
      disabled={uploading}
    />
  );
}

Image Display with Storage URLs

function ImageGallery() {
  const images = useQuery(api.images.list) || [];

  return (
    <div className="grid grid-cols-3 gap-4">
      {images.map((image) => (
        <ImageWithUrl key={image._id} storageId={image.storageId} />
      ))}
    </div>
  );
}

function ImageWithUrl({ storageId }: { storageId: Id<"_storage"> }) {
  const url = useQuery(api.files.getUrl, { storageId });

  if (url === undefined) {
    return <div className="animate-pulse bg-gray-200 h-48" />;
  }

  if (url === null) {
    return <div>Image not found</div>;
  }

  return <img src={url} alt="" className="w-full h-48 object-cover" />;
}

Provider Setup

// main.tsx or _app.tsx
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ConvexAuthProvider } from "@convex-dev/auth/react";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

function App() {
  return (
    <ConvexAuthProvider client={convex}>
      <YourApp />
    </ConvexAuthProvider>
  );
}

Best Practices

1. Never Call Hooks Conditionally

// WRONG
if (isLoggedIn) {
  const data = useQuery(api.data.get);
}

// CORRECT
const data = useQuery(api.data.get, isLoggedIn ? {} : "skip");

2. Handle All States

function DataDisplay() {
  const data = useQuery(api.data.get);

  // Always handle: undefined (loading), null (not found), and data
  if (data === undefined) return <Loading />;
  if (data === null) return <NotFound />;
  return <Content data={data} />;
}

3. Use TypeScript Properly

import { Id } from "../convex/_generated/dataModel";

interface Props {
  userId: Id<"users">;  // Use Id<> type, not string
}

4. Avoid Prop Drilling with Queries

// Instead of passing data through many components,
// query it where needed
function DeepNestedComponent({ itemId }: { itemId: Id<"items"> }) {
  // Query directly in the component that needs it
  const item = useQuery(api.items.get, { id: itemId });
  // ...
}

5. Do NOT Use External UI Libraries Unless Specified

If you want to use a UI element, you MUST create it. DO NOT use external libraries like Shadcn/UI unless explicitly asked.

6. Do NOT Use sharp for Image Compression

Always use canvas for image compression, not sharp.

You Might Also Like

Related Skills

cache-components

cache-components

137Kdev-frontend

Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.

vercel avatarvercel
Obtenir
component-refactoring

component-refactoring

128Kdev-frontend

Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component --json` shows complexity > 50 or lineCount > 300, when the user asks for code splitting, hook extraction, or complexity reduction, or when `pnpm analyze-component` warns to refactor before testing; avoid for simple/well-structured components, third-party wrappers, or when the user explicitly wants testing without refactoring.

langgenius avatarlanggenius
Obtenir
web-artifacts-builder

web-artifacts-builder

47Kdev-frontend

Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.

anthropics avataranthropics
Obtenir
frontend-design

frontend-design

47Kdev-frontend

Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.

anthropics avataranthropics
Obtenir
react-modernization

react-modernization

28Kdev-frontend

Upgrade React applications to latest versions, migrate from class components to hooks, and adopt concurrent features. Use when modernizing React codebases, migrating to React Hooks, or upgrading to latest React versions.

wshobson avatarwshobson
Obtenir
tailwind-design-system

tailwind-design-system

28Kdev-frontend

Build scalable design systems with Tailwind CSS v4, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns.

wshobson avatarwshobson
Obtenir