> ## 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](/concepts/schema-restrictions) defined in your [function specs](/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](/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>,
  });
};
```

## `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>` |
| Pass `"skip"` to disable query subscription          | Same—pass `"skip"` to disable query subscription                                                       |
