Skip to main content

Documentation Index

Fetch the complete documentation index at: https://confect.dev/llms.txt

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

@confect/react provides drop-in replacements for Convex’s React hooks. Each hook automatically encodes your args and decodes return values through the Effect Schemas defined in your function specs. You work with the Schema Type (decoded) values on both sidesβ€”the hooks handle the round-trip to Convex’s Encoded representation transparently. Functions are referenced via refs (from confect/_generated/refs) instead of Convex’s api object. Each ref carries the args, returns, and (optionally) error schemas from the corresponding function spec, which is what enables the automatic encoding and decoding.

Setup

The React provider setup is the same as vanilla Convex.
src/main.tsx
import { ConvexProvider, ConvexReactClient } from "convex/react";
import React from "react";
import ReactDOM from "react-dom/client";

import App from "./App";

const convexClient = new ConvexReactClient(
  import.meta.env.VITE_CONVEX_URL,
);

ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement,
).render(
  <React.StrictMode>
    <ConvexProvider client={convexClient}>
      <App />
    </ConvexProvider>
  </React.StrictMode>,
);

useQuery

Encodes args using the spec’s args schema, passes them to Convex, and decodes the result using the spec’s returns schema. Returns a QueryResult<A, E>β€”a tagged union with Loading, Success, and Failure variants. Given this spec:
confect/notes.spec.ts
import { FunctionSpec, GroupSpec } from "@confect/core";
import { Schema } from "effect";

import { Notes } from "./tables/Notes";

export const notes = GroupSpec.make("notes").addFunction(
  FunctionSpec.publicQuery({
    name: "list",
    args: Schema.Struct({}),
    returns: Schema.Array(Notes.Doc),
  }),
);
The hook accepts {} (the Type of Schema.Struct({})) as args and returns a QueryResult<readonly Notes.Doc["Type"][]>. Match it with QueryResult.match:
import { QueryResult, useQuery } from "@confect/react";
import refs from "../confect/_generated/refs";

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

  return QueryResult.match(notes, {
    onLoading: () => <p>Loading…</p>,
    onSuccess: (notes) => (
      <ul>
        {notes.map((note) => (
          <li key={note._id}>{note.text}</li>
        ))}
      </ul>
    ),
  });
};
QueryResult also exposes the lower-level predicates QueryResult.isLoading, QueryResult.isSuccess, and QueryResult.isFailure for cases where pattern matching is awkward.

Typed errors

When the ref’s spec declares an error schema, useQuery returns QueryResult<A, E> and QueryResult.match requires an onFailure handler that receives the decoded typed error. See Error Handling for how to declare error schemas.
import { QueryResult, useQuery } from "@confect/react";
import refs from "../confect/_generated/refs";

const NoteLookup = ({ noteId }: { noteId: string }) => {
  const lookup = useQuery(refs.public.notes.getOrFail, { noteId });

  return (
    <div>
      {QueryResult.match(lookup, {
        onLoading: () => "Looking up…",
        onSuccess: (note) => `Found: ${note.text}`,
        onFailure: (error) => `Note ${error.noteId} not found.`,
      })}
    </div>
  );
};
Failures that are not declared in the error schema are not surfaced as Failure. They propagate the same way they do with convex/react’s useQuery (typically reaching the nearest error boundary).

Skipping queries

Pass "skip" instead of args to disable the query subscription. The hook returns a Loading variant whose skipped flag is true, which lets you distinguish a query that is genuinely in flight from one sitting idle because no args have been provided.
import { QueryResult, useQuery } from "@confect/react";
import refs from "../confect/_generated/refs";

const NoteDetail = ({
  selectedId,
}: {
  selectedId: string | undefined;
}) => {
  const note = useQuery(
    refs.public.notes.get,
    selectedId !== undefined ? { id: selectedId } : "skip",
  );

  return QueryResult.match(note, {
    onLoading: (skipped) => (
      <p>{skipped ? "Select a note" : "Loading…"}</p>
    ),
    onSuccess: (note) => <p>{note.text}</p>,
  });
};

useMutation

Returns a function that encodes args using the spec’s args schema, calls the Convex mutation, and decodes the result using the spec’s returns schema. The returned promise’s shape depends on whether the spec declares an error schema.

Without an error schema

The function returns Promise<A>, matching convex/react’s useMutation. Undeclared failures still reject the promise.
confect/notes.spec.ts
FunctionSpec.publicMutation({
  name: "insert",
  args: Schema.Struct({ text: Schema.String }),
  returns: GenericId.GenericId("notes"),
});
import { useMutation } from "@confect/react";
import refs from "../confect/_generated/refs";

const InsertNote = () => {
  const insertNote = useMutation(refs.public.notes.insert);

  return (
    <button onClick={() => void insertNote({ text: "Hello" })}>
      Insert note
    </button>
  );
};

With an error schema

When the spec declares an error schema, the function returns Promise<Either<A, E>>. Unwrap with Either.match (or another Either combinator) to handle both branches. Undeclared failures still reject the promise.
confect/notes.spec.ts
FunctionSpec.publicMutation({
  name: "deleteOrFail",
  args: Schema.Struct({ noteId: GenericId.GenericId("notes") }),
  returns: Schema.Null,
  error: Schema.Union(NoteNotFound, Forbidden),
});
import { useMutation } from "@confect/react";
import { Either } from "effect";
import refs from "../confect/_generated/refs";

const DeleteNote = ({ noteId }: { noteId: string }) => {
  const deleteOrFail = useMutation(refs.public.notes.deleteOrFail);

  const handleClick = async () => {
    const result = await deleteOrFail({ noteId });
    Either.match(result, {
      onLeft: (error) => console.error(error._tag, error),
      onRight: () => console.log("deleted"),
    });
  };

  return <button onClick={handleClick}>Delete</button>;
};

useAction

Same shape as useMutation: returns Promise<A> when the ref has no error schema, and Promise<Either<A, E>> when it does.
confect/random.spec.ts
FunctionSpec.publicAction({
  name: "getNumber",
  args: Schema.Struct({}),
  returns: Schema.Number,
});
import { useAction } from "@confect/react";
import refs from "../confect/_generated/refs";

const RandomNumber = () => {
  const getRandom = useAction(refs.public.random.getNumber);

  const handleClick = () => {
    void getRandom({}).then(console.log);
  };

  return <button onClick={handleClick}>Get random</button>;
};

Differences from vanilla Convex hooks

Vanilla ConvexConfect
Functions referenced via api.module.fnFunctions referenced via refs
Args passed directly to Convex as-isArgs are schema-encoded from Type to Encoded before sending to Convex
Return values received directly from Convex as-isReturn values are schema-decoded from Encoded to Type before returning
useQuery returns T | undefineduseQuery returns QueryResult<A, E>
Cannot distinguish loading from skippedLoading carries a skipped: boolean flag
useMutation/useAction always return Promise<T>Refs with an error schema return Promise<Either<A, E>>; refs without one still return Promise<A>
Pass "skip" to disable query subscriptionSameβ€”pass "skip" to disable query subscription