> ## Documentation Index
> Fetch the complete documentation index at: https://confect.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# React

> Use Confect's React hooks to call your functions from the client.

`@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](/v9/concepts/schema-restrictions) defined in your [function specs](/v9/concepts/spec-impl-model). 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.

```tsx src/main.tsx theme={null}
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:

```ts confect/notes.spec.ts theme={null}
import { FunctionSpec, GroupSpec } from "@confect/core";
import * as Schema from "effect/Schema";

import notes from "./_generated/tables/notes";

export default GroupSpec.make().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`:

```tsx theme={null}
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](/v9/server/error-handling) for how to declare error schemas.

```tsx theme={null}
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](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-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.

```tsx theme={null}
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>,
  });
};
```

## `usePaginatedQuery`

Loads data reactively from a [paginated query](/v9/server/functions#paginated-queries), mirroring the ergonomics of `usePaginatedQuery` from `convex/react`. The ref must come from a spec defined with `FunctionSpec.publicPaginatedQuery` — passing any other ref fails at runtime with an error pointing at the constructor.

Args are encoded using the spec's user-args schema (`paginationOpts` is managed by the hook, not the caller), and each loaded page is decoded using the spec's `item` schema. Pass `"skip"` instead of args to disable the query, like `useQuery`.

Returns a `PaginatedQueryResult<Item, E>`. The loaded variants — `LoadingFirstPage`, `LoadingMore`, `CanLoadMore`, and `Exhausted` — all carry `results` and `isLoading`, and `CanLoadMore` additionally carries `loadMore`, so the common UI needs only field access and a couple of predicates:

```tsx theme={null}
import { PaginatedQueryResult, usePaginatedQuery } from "@confect/react";
import refs from "../confect/_generated/refs";

const PaginatedNoteList = () => {
  const notes = usePaginatedQuery(
    refs.public.notes.listByAuthor,
    { author: "ada" },
    { initialNumItems: 10 },
  );

  return (
    <div>
      <ul>
        {notes.results.map((note) => (
          <li key={note._id}>{note.text}</li>
        ))}
      </ul>
      {notes.isLoading && <p>Loading…</p>}
      {PaginatedQueryResult.isCanLoadMore(notes) && (
        <button type="button" onClick={() => notes.loadMore(10)}>
          Load more
        </button>
      )}
    </div>
  );
};
```

`loadMore` lives only on `CanLoadMore`, the one state it can make progress from — the underlying Convex hook exposes it on every status, but calling it while a page is in flight, once the list is exhausted, or after a failure is an intentional no-op there. Narrowing with `isCanLoadMore` (or `match`'s `onCanLoadMore`) makes that statically apparent instead of silently dropping the call.

`PaginatedQueryResult` also provides `match` for exhaustive pattern matching, and the predicates `isLoadingFirstPage`, `isLoadingMore`, `isExhausted`, `isLoading`, and `isFailure`.

### Typed errors

When the spec declares an `error` schema, the result type gains a `Failure` variant carrying the decoded typed error — errors are values, consistent with `useQuery`. Narrow with `PaginatedQueryResult.isFailure` (or handle `onFailure` in `match`) to reach it:

```tsx theme={null}
const notes = usePaginatedQuery(
  refs.public.notes.listOrFail,
  {},
  { initialNumItems: 10 },
);

if (PaginatedQueryResult.isFailure(notes)) {
  // `results` holds the pages loaded before the failure, so they can still
  // be rendered alongside the error.
  return (
    <div>
      <NoteList notes={notes.results} />
      <p>Failed to load more: {notes.error.reason}</p>
    </div>
  );
}
// notes: narrowed to the non-`Failure` variants — isLoading is available too.
```

Every variant carries `results`, `Failure` included: when a later page fails, the pages already loaded are not discarded.

Without an `error` schema, the `Failure` variant is excluded from the type entirely, so no narrowing is needed. Failures not declared in the `error` schema are thrown during render and propagate to the nearest [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary).

<Note>`usePaginatedQuery` requires `convex` 1.36.0 or newer.</Note>

## `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.

```ts confect/notes.spec.ts theme={null}
import { Id } from "./_generated/id";

FunctionSpec.publicMutation({
  name: "insert",
  args: () => Schema.Struct({ text: Schema.String }),
  returns: () => Id("notes"),
});
```

```tsx theme={null}
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.

```ts confect/notes.spec.ts theme={null}
import { Id } from "./_generated/id";

FunctionSpec.publicMutation({
  name: "deleteOrFail",
  args: () => Schema.Struct({ noteId: Id("notes") }),
  returns: () => Schema.Null,
  error: () => Schema.Union(NoteNotFound, Forbidden),
});
```

```tsx theme={null}
import { useMutation } from "@confect/react";
import * as Either from "effect/Either";
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.

```ts confect/random.spec.ts theme={null}
FunctionSpec.publicAction({
  name: "getNumber",
  args: () => Schema.Struct({}),
  returns: () => Schema.Number,
});
```

```tsx theme={null}
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 Convex                                        | Confect                                                                                                 |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Functions referenced via `api.module.fn`              | Functions referenced via `refs`                                                                         |
| Args passed directly to Convex as-is                  | Args are schema-encoded from `Type` to `Encoded` before sending to Convex                               |
| Return values received directly from Convex as-is     | Return values are schema-decoded from `Encoded` to `Type` before returning                              |
| `useQuery` returns `T \| undefined`                   | `useQuery` returns `QueryResult<A, E>`                                                                  |
| Cannot distinguish loading from skipped               | `Loading` 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>`  |
| `usePaginatedQuery` throws query errors during render | `usePaginatedQuery` returns `PaginatedQueryResult<Item, E>` with declared errors as a `Failure` variant |
| Pass `"skip"` to disable query subscription           | Same—pass `"skip"` to disable query subscription                                                        |
