--- 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. license: MIT metadata: author: convex-community 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 ```tsx 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 (

Convex Chat

{name}

setNewMessageText(event.target.value)} placeholder="Write a message..." />
); } ``` --- # 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 ```tsx function UserProfile({ userId }: { userId: Id<"users"> }) { const user = useQuery(api.users.get, { userId }); // Loading state if (user === undefined) { return
Loading...
; } // Not found if (user === null) { return
User not found
; } // Data loaded return
{user.name}
; } ``` --- # Conditional Queries with "skip" ## CRITICAL: Never Use Hooks Conditionally ```tsx // 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 ```tsx 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 ; } if (user === null) { return ; } return (
); } ``` --- # useMutation Hook ## Basic Usage ```tsx 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 (
setTitle(e.target.value)} placeholder="Title" disabled={isSubmitting} />