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>
);
}
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