Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rjdellecese/confect/llms.txt

Use this file to discover all available pages before exploring further.

The @confect/react package provides React hooks for querying and mutating data with full type safety.

Overview

useQuery

Subscribe to real-time data from queries

useMutation

Execute mutations to modify data

useAction

Run actions for side effects

Installation

The React package is built on top of Convex’s React client:
npm install @confect/react convex react

Setup

Wrap your app with the Convex provider:
import { ConvexProvider, ConvexReactClient } from "convex/react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

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

useQuery

Subscribe to real-time query results with automatic updates.

Type Signature

packages/react/src/index.ts
export const useQuery = <Query extends Ref.AnyPublicQuery>(
  ref: Query,
  args: Ref.Args<Query>["Type"],
): Ref.Returns<Query>["Type"] | undefined

Basic Usage

import { useQuery } from "@confect/react";
import { refs } from "../confect/_generated/refs";

function UserList() {
  const users = useQuery(refs.public.users.list, {});
  
  if (users === undefined) {
    return <div>Loading...</div>;
  }
  
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

With Arguments

function UserProfile({ userId }: { userId: string }) {
  const user = useQuery(refs.public.users.getById, { id: userId });
  
  if (user === undefined) {
    return <div>Loading user...</div>;
  }
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>Role: {user.role}</p>
    </div>
  );
}

Loading States

useQuery returns undefined while loading. The query automatically subscribes to updates and re-renders when data changes.
function Posts() {
  const posts = useQuery(refs.public.posts.list, {
    limit: 10
  });
  
  if (posts === undefined) {
    return (
      <div className="loading">
        <Spinner />
        <p>Loading posts...</p>
      </div>
    );
  }
  
  if (posts.length === 0) {
    return <div>No posts yet.</div>;
  }
  
  return (
    <div>
      {posts.map((post) => (
        <PostCard key={post._id} post={post} />
      ))}
    </div>
  );
}

Conditional Queries

function ConditionalData({ enabled }: { enabled: boolean }) {
  // Skip query if not enabled
  const data = useQuery(
    refs.public.data.fetch,
    enabled ? {} : "skip" as any
  );
  
  if (!enabled) {
    return <div>Query disabled</div>;
  }
  
  return <div>{data ? JSON.stringify(data) : "Loading..."}</div>;
}

useMutation

Execute mutations to modify data in your database.

Type Signature

packages/react/src/index.ts
export const useMutation = <Mutation extends Ref.AnyPublicMutation>(
  ref: Mutation,
) => (
  args: Ref.Args<Mutation>["Type"],
): Promise<Ref.Returns<Mutation>["Type"]>

Basic Usage

import { useMutation } from "@confect/react";
import { refs } from "../confect/_generated/refs";
import { useState } from "react";

function CreateUser() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const createUser = useMutation(refs.public.users.create);
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    try {
      const userId = await createUser({ name, email });
      console.log("Created user:", userId);
      
      // Reset form
      setName("");
      setEmail("");
    } catch (error) {
      console.error("Failed to create user:", error);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
        required
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <button type="submit">Create User</button>
    </form>
  );
}

With Loading State

function UpdateProfile() {
  const [isLoading, setIsLoading] = useState(false);
  const updateProfile = useMutation(refs.public.users.updateProfile);
  
  const handleUpdate = async (newName: string) => {
    setIsLoading(true);
    try {
      await updateProfile({ name: newName });
      alert("Profile updated!");
    } catch (error) {
      alert("Update failed: " + error.message);
    } finally {
      setIsLoading(false);
    }
  };
  
  return (
    <button 
      onClick={() => handleUpdate("New Name")}
      disabled={isLoading}
    >
      {isLoading ? "Updating..." : "Update Profile"}
    </button>
  );
}

Optimistic Updates

function LikeButton({ postId }: { postId: string }) {
  const likePost = useMutation(refs.public.posts.like);
  const [optimisticLikes, setOptimisticLikes] = useState(0);
  
  const post = useQuery(refs.public.posts.getById, { id: postId });
  
  const handleLike = async () => {
    // Optimistic update
    setOptimisticLikes((prev) => prev + 1);
    
    try {
      await likePost({ postId });
    } catch (error) {
      // Rollback on error
      setOptimisticLikes((prev) => prev - 1);
      console.error("Failed to like:", error);
    }
  };
  
  const displayLikes = post ? post.likes + optimisticLikes : 0;
  
  return (
    <button onClick={handleLike}>
      ❤️ {displayLikes} likes
    </button>
  );
}

Complex Form Example

function CreatePost() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [tags, setTags] = useState<string[]>([]);
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const createPost = useMutation(refs.public.posts.create);
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      const postId = await createPost({
        title,
        content,
        tags
      });
      
      console.log("Post created:", postId);
      
      // Navigate to post or reset form
      setTitle("");
      setContent("");
      setTags([]);
    } catch (error) {
      console.error("Failed to create post:", error);
      alert("Failed to create post");
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Title"
        required
      />
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="Content"
        required
      />
      <TagInput value={tags} onChange={setTags} />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Creating..." : "Create Post"}
      </button>
    </form>
  );
}

useAction

Execute actions for side effects like sending emails or calling external APIs.

Type Signature

packages/react/src/index.ts
export const useAction = <Action extends Ref.AnyPublicAction>(
  ref: Action,
) => (
  args: Ref.Args<Action>["Type"],
): Promise<Ref.Returns<Action>["Type"]>

Basic Usage

import { useAction } from "@confect/react";
import { refs } from "../confect/_generated/refs";

function SendEmailButton({ userId }: { userId: string }) {
  const sendEmail = useAction(refs.public.emails.send);
  const [isSending, setIsSending] = useState(false);
  
  const handleSend = async () => {
    setIsSending(true);
    try {
      const result = await sendEmail({
        to: userId,
        subject: "Hello!",
        body: "This is a test email."
      });
      
      if (result.success) {
        alert("Email sent!");
      } else {
        alert("Email failed to send");
      }
    } catch (error) {
      console.error("Error sending email:", error);
    } finally {
      setIsSending(false);
    }
  };
  
  return (
    <button onClick={handleSend} disabled={isSending}>
      {isSending ? "Sending..." : "Send Email"}
    </button>
  );
}

File Upload Example

function FileUpload() {
  const processFile = useAction(refs.public.files.process);
  const [isProcessing, setIsProcessing] = useState(false);
  
  const handleFileUpload = async (file: File) => {
    setIsProcessing(true);
    
    try {
      const arrayBuffer = await file.arrayBuffer();
      const base64 = btoa(
        String.fromCharCode(...new Uint8Array(arrayBuffer))
      );
      
      const result = await processFile({
        fileName: file.name,
        content: base64
      });
      
      console.log("File processed:", result);
      alert("File uploaded successfully!");
    } catch (error) {
      console.error("Upload failed:", error);
      alert("Upload failed");
    } finally {
      setIsProcessing(false);
    }
  };
  
  return (
    <div>
      <input
        type="file"
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (file) handleFileUpload(file);
        }}
        disabled={isProcessing}
      />
      {isProcessing && <p>Processing...</p>}
    </div>
  );
}

External API Call

function WeatherWidget({ city }: { city: string }) {
  const [weather, setWeather] = useState<any>(null);
  const fetchWeather = useAction(refs.public.weather.fetch);
  
  useEffect(() => {
    let mounted = true;
    
    fetchWeather({ city }).then((data) => {
      if (mounted) setWeather(data);
    });
    
    return () => { mounted = false; };
  }, [city, fetchWeather]);
  
  if (!weather) {
    return <div>Loading weather...</div>;
  }
  
  return (
    <div>
      <h2>Weather in {city}</h2>
      <p>Temperature: {weather.temperature}°C</p>
      <p>Condition: {weather.condition}</p>
    </div>
  );
}

Type Safety

All hooks provide full type safety through TypeScript.

Argument Types

Arguments are validated against your schema

Return Types

Return values are properly typed

Auto-complete

Get IDE suggestions for all properties

Compile-time Errors

Catch errors before runtime
// TypeScript knows the exact shape of args and return types
const user = useQuery(
  refs.public.users.getById,
  { id: "123" } // ✅ Typed correctly
  // { userId: "123" } // ❌ Type error: wrong property name
);

// Return type is inferred
if (user) {
  console.log(user.name); // ✅ TypeScript knows 'name' exists
  // console.log(user.foo); // ❌ Type error: 'foo' doesn't exist
}

Real-time Updates

Queries created with useQuery automatically subscribe to real-time updates. When data changes on the server, your components re-render automatically.
function LiveUserCount() {
  const users = useQuery(refs.public.users.list, {});
  
  // This component automatically updates when users are added/removed
  return (
    <div>
      <h2>Active Users</h2>
      <p>{users?.length ?? 0} users online</p>
    </div>
  );
}

Best Practices

Handle Loading

Always check for undefined from useQuery

Error Handling

Use try-catch for mutations and actions

Loading States

Show feedback during async operations

Optimistic UI

Update UI before server confirmation
// ✅ Good: Handle all states
function UserProfile({ userId }: { userId: string }) {
  const user = useQuery(refs.public.users.getById, { id: userId });
  
  if (user === undefined) {
    return <LoadingSpinner />;
  }
  
  if (user === null) {
    return <NotFound />;
  }
  
  return <UserCard user={user} />;
}

// ❌ Bad: No loading state
function BadExample({ userId }: { userId: string }) {
  const user = useQuery(refs.public.users.getById, { id: userId });
  return <div>{user.name}</div>; // Crashes if user is undefined!
}

Next Steps

Core API

Learn about Confect core types

Server API

Implement backend functions

Testing

Test your React components

CLI

Use the CLI to manage your project

Build docs developers (and LLMs) love