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.

Confect provides React hooks that seamlessly integrate with Effect schemas, giving you end-to-end type safety from your database to your UI components. These hooks handle encoding/decoding automatically and provide real-time updates.

Installation

npm install @confect/react

Setup

Wrap your application with ConvexProvider from convex/react:
App.tsx
import { ConvexProvider, ConvexReactClient } from "convex/react";

const App = () => {
  const convexClient = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

  return (
    <ConvexProvider client={convexClient}>
      <YourApp />
    </ConvexProvider>
  );
};

Core Hooks

Confect provides three primary hooks for interacting with your backend:

useQuery

Subscribe to real-time data

useMutation

Modify data transactionally

useAction

Execute long-running operations

useQuery

Subscribe to query results with automatic updates. The hook returns undefined while loading, then the decoded result.

Type Signature

const useQuery = <Query extends Ref.AnyPublicQuery>(
  ref: Query,
  args: Ref.Args<Query>["Type"],
): Ref.Returns<Query>["Type"] | undefined

Example: Display a List

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

const NoteList = () => {
  const notes = useQuery(refs.public.notesAndRandom.notes.list, {});

  if (notes === undefined) {
    return <p>Loading…</p>;
  }

  return (
    <ul>
      {notes.map((note) => (
        <li key={note._id}>
          <p>{note.text}</p>
          <p>Created: {new Date(note._creationTime).toLocaleString()}</p>
        </li>
      ))}
    </ul>
  );
};

How It Works

  1. Automatic encoding: Arguments are encoded using your Effect schema
  2. Real-time subscription: Convex keeps the data synchronized
  3. Automatic decoding: Results are decoded back to TypeScript types
  4. Type safety: Full inference from function reference to result type
The query automatically re-runs when dependencies change, keeping your UI in sync with the database.

useMutation

Execute transactional database operations. Returns a function you can call to trigger the mutation.

Type Signature

const useMutation = <Mutation extends Ref.AnyPublicMutation>(
  ref: Mutation,
) => (
  args: Ref.Args<Mutation>["Type"],
) => Promise<Ref.Returns<Mutation>["Type"]>

Example: Create and Delete

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

const NoteForm = () => {
  const [note, setNote] = useState("");
  const insertNote = useMutation(refs.public.notesAndRandom.notes.insert);
  const deleteNote = useMutation(refs.public.notesAndRandom.notes.delete_);

  const handleSubmit = async () => {
    // insertNote is fully typed - TypeScript knows the args and return type
    const noteId = await insertNote({ text: note });
    setNote("");
    console.log(`Created note with ID: ${noteId}`);
  };

  const handleDelete = async (noteId: string) => {
    await deleteNote({ noteId });
  };

  return (
    <div>
      <textarea
        value={note}
        onChange={(e) => setNote(e.target.value)}
        placeholder="Write a note..."
      />
      <button onClick={handleSubmit}>Save Note</button>
    </div>
  );
};

Mutation Properties

  • Transactional: All database operations succeed or fail together
  • Optimistic updates: Queries automatically reflect changes
  • Error handling: Promise rejections for failed mutations
Mutations run in a transaction and cannot make external HTTP requests. Use actions for non-transactional work.

useAction

Execute actions that can run longer operations, make HTTP requests, or call third-party APIs.

Type Signature

const useAction = <Action extends Ref.AnyPublicAction>(
  ref: Action,
) => (
  args: Ref.Args<Action>["Type"],
) => Promise<Ref.Returns<Action>["Type"]>

Example: Call External API

RandomNumber.tsx
import { useAction } from "@confect/react";
import { useEffect, useState } from "react";
import refs from "../confect/_generated/refs";

const RandomNumber = () => {
  const [randomNumber, setRandomNumber] = useState<number | null>(null);
  const [loading, setLoading] = useState(false);
  const getRandom = useAction(refs.public.notesAndRandom.random.getNumber);

  const fetchNumber = async () => {
    setLoading(true);
    try {
      const number = await getRandom({});
      setRandomNumber(number);
    } catch (error) {
      console.error("Failed to fetch random number:", error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchNumber();
  }, []);

  return (
    <div>
      <p>Random number: {randomNumber ?? "Loading…"}</p>
      <button onClick={fetchNumber} disabled={loading}>
        {loading ? "Fetching…" : "Get New Number"}
      </button>
    </div>
  );
};

Example: Send Email

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

const EmailSender = () => {
  const [status, setStatus] = useState<string | null>(null);
  const sendEmail = useAction(refs.public.node.email.send);

  const handleSend = async () => {
    setStatus("Sending…");
    try {
      await sendEmail({
        to: "user@example.com",
        subject: "Welcome to Confect",
        body: "Thanks for trying Confect!",
      });
      setStatus("Email sent successfully!");
    } catch (error) {
      setStatus(`Error: ${String(error)}`);
    }
  };

  return (
    <div>
      <button onClick={handleSend}>Send Email</button>
      {status && <p>{status}</p>}
    </div>
  );
};

Action Capabilities

  • HTTP requests: Call external APIs and webhooks
  • Long-running: No strict time limits like mutations
  • Non-transactional: Can have side effects
  • Node runtime: Access to Node.js APIs in Node actions

Effect Schema Integration

All hooks work seamlessly with Effect schemas, providing automatic validation and transformation.

Define Your Schema

confect/spec/notes.ts
import { FunctionSpec, GenericId, GroupSpec } from "@confect/core";
import { Schema } from "effect";
import { Notes } from "../tables/Notes";

export const notes = GroupSpec.make("notes")
  .addFunction(
    FunctionSpec.publicMutation({
      name: "insert",
      args: Schema.Struct({ text: Schema.String }),
      returns: GenericId.GenericId("notes"),
    }),
  )
  .addFunction(
    FunctionSpec.publicQuery({
      name: "list",
      args: Schema.Struct({}),
      returns: Schema.Array(Notes.Doc),
    }),
  );

Use in Components

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

// TypeScript knows the exact shape of the data
const notes = useQuery(refs.public.notesAndRandom.notes.list, {});
// Type: Array<{ _id: string; _creationTime: number; text: string }> | undefined

const insertNote = useMutation(refs.public.notesAndRandom.notes.insert);
// Type: (args: { text: string }) => Promise<string>
Confect automatically handles encoding and decoding between TypeScript types and database representations using your Effect schemas.

Complete Example

Here’s a full-featured component using all three hooks:
NotesApp.tsx
import { useAction, useMutation, useQuery } from "@confect/react";
import { useState } from "react";
import refs from "../confect/_generated/refs";

const NotesApp = () => {
  const [note, setNote] = useState("");
  
  // Query: Real-time list of notes
  const notes = useQuery(refs.public.notesAndRandom.notes.list, {});
  
  // Mutations: Transactional database operations
  const insertNote = useMutation(refs.public.notesAndRandom.notes.insert);
  const deleteNote = useMutation(refs.public.notesAndRandom.notes.delete_);
  
  // Action: External API call
  const getRandom = useAction(refs.public.notesAndRandom.random.getNumber);

  const handleInsert = async () => {
    await insertNote({ text: note });
    setNote("");
  };

  const handleDelete = async (noteId: string) => {
    await deleteNote({ noteId });
  };

  const addRandomNote = async () => {
    const randomNumber = await getRandom({});
    await insertNote({ text: `Random number: ${randomNumber}` });
  };

  if (notes === undefined) {
    return <div>Loading notes…</div>;
  }

  return (
    <div>
      <h1>My Notes</h1>
      
      {/* Create form */}
      <div>
        <textarea
          value={note}
          onChange={(e) => setNote(e.target.value)}
          placeholder="Write a note..."
        />
        <button onClick={handleInsert}>Add Note</button>
        <button onClick={addRandomNote}>Add Random Note</button>
      </div>

      {/* Notes list */}
      <ul>
        {notes.map((note) => (
          <li key={note._id}>
            <p>{note.text}</p>
            <button onClick={() => handleDelete(note._id)}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default NotesApp;

Real-Time Updates

Queries automatically update when underlying data changes:
LiveCounter.tsx
import { useQuery } from "@confect/react";
import refs from "../confect/_generated/refs";

const LiveCounter = () => {
  // This automatically updates when notes are added or deleted
  const notes = useQuery(refs.public.notesAndRandom.notes.list, {});

  return (
    <div>
      <h2>Total Notes: {notes?.length ?? "Loading…"}</h2>
    </div>
  );
};
Changes made in one tab, by another user, or by scheduled functions automatically propagate to all connected clients.

Error Handling

Query Errors

Queries handle errors gracefully by returning undefined:
const notes = useQuery(refs.public.notes.list, {});

if (notes === undefined) {
  // Either loading or error occurred
  return <div>Loading…</div>;
}

Mutation and Action Errors

Handle promise rejections:
const insertNote = useMutation(refs.public.notes.insert);

try {
  await insertNote({ text: "New note" });
} catch (error) {
  console.error("Failed to insert note:", error);
  // Show error UI
}

Type Safety

Confect provides complete type inference:
// TypeScript infers everything from your schema
const notes = useQuery(refs.public.notes.list, {});
//    ^? Array<{ _id: string; text: string; ... }> | undefined

const insertNote = useMutation(refs.public.notes.insert);
//    ^? (args: { text: string }) => Promise<string>

// TypeScript catches errors
insertNote({ text: 123 }); // Error: Type 'number' is not assignable to type 'string'
insertNote({ title: "..." }); // Error: Object literal may only specify known properties

Best Practices

Use Type Inference

Let TypeScript infer types from your function references rather than manually typing results

Handle Loading States

Always check for undefined when using useQuery before rendering data

Catch Errors

Wrap mutation and action calls in try-catch blocks for proper error handling

Keep Refs Generated

Run npx @confect/cli codegen to regenerate refs after changing function specs

Next Steps

HTTP Client

Learn how to call Confect APIs over HTTP

Server Functions

Define queries, mutations, and actions

Database

Work with the Confect database

Testing

Test your Confect application