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