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

# React SDK

> @pylonsync/react — hooks for queries, mutations, sessions, search, presence, multiplayer.

`@pylonsync/react` is the React client. The `db.*` namespace exposes hooks that read from a local sync replica — every component using a hook re-renders automatically when a row changes (your own mutation, a server push, a write from another tab).

## Install

```bash theme={null}
bun add @pylonsync/react
# or: npm i @pylonsync/react
```

## Initialize once

Pick any entry point that runs before your first hook (`src/main.tsx`, a Next.js root client provider, a top-level component effect):

```ts theme={null}
import { init } from "@pylonsync/react";

init({ baseUrl: "http://localhost:4321" });
```

In Next.js + Vercel, omit `baseUrl` (or pass `""`) so the client talks same-origin and the [Next rewrite](/operations/vercel) forwards `/api/*` to your Pylon backend.

## The `db.*` namespace

`db` is the public API for apps using a single global sync engine. Every method below subscribes to or writes to the same shared replica.

### `db.useQuery` — live list

```tsx theme={null}
import { db } from "@pylonsync/react";

type Todo = { id: string; title: string; done: boolean };

export function TodoList() {
  const { data: todos, loading, error } = db.useQuery<Todo>("Todo");
  if (loading) return <Spinner />;
  if (error) return <ErrorBanner error={error} />;
  return <ul>{todos.map((t) => <li key={t.id}>{t.title}</li>)}</ul>;
}
```

With filtering / ordering / limits:

```tsx theme={null}
const { data } = db.useQuery<Todo>("Todo", {
  where: { authorId: "u_xyz", done: false },
  orderBy: { createdAt: "desc" },
  limit: 50,
});
```

### `db.useQueryOne` — single row by ID

```tsx theme={null}
const { data: post } = db.useQueryOne<Post>("Post", postId);
```

### `db.useInfiniteQuery` — paginated with `loadMore`

```tsx theme={null}
const { data, hasMore, loadMore, loading } = db.useInfiniteQuery<Post>("Post", {
  pageSize: 20,
});
```

### `db.useReactiveQuery` — server-side query, auto re-runs

For Convex-style server functions that the framework re-runs whenever their dependency set changes:

```tsx theme={null}
const { data: feed, loading } = db.useReactiveQuery<FeedItem[]>("getFeed", {
  userId: currentUser.id,
});
```

The server records every `ctx.db.*` read inside your `query()` handler and re-runs the handler when any of those rows mutate. See [Reactive queries](/concepts/reactive-queries).

### `db.useMutation` — server function w/ optimistic updates

```tsx theme={null}
const send = db.useMutation<
  { channelId: string; body: string },
  { messageId: string }
>("sendMessage", {
  optimistic: (args, ctx) => ({
    entity: "Message",
    data: {
      id: ctx.id,
      channelId: args.channelId,
      body: args.body,
      authorId: me.id,
      createdAt: ctx.now,
    },
  }),
});

await send.mutate({ channelId, body: "Hi!" });
```

The ghost row appears in `db.useQuery("Message", ...)` instantly; the server's broadcast reconciles in-place. See [Optimistic updates](/concepts/optimistic-updates).

### `db.useEntity` — optimistic CRUD bound to one entity

When you'd rather call `insert/update/delete` directly than wire a server function:

```tsx theme={null}
const { insert, update, remove } = db.useEntity("Todo");

await insert({ title: "Buy milk", done: false });
await update(todoId, { done: true });
await remove(todoId);
```

Same store + reconciliation as `db.useMutation`.

### `db.useSearch` — live faceted search

```tsx theme={null}
const { hits, facetCounts, total } = db.useSearch<Product>("Product", {
  query: "red sneakers",
  filters: { category: "shoes" },
  facets: ["brand", "color"],
  sort: ["price", "desc"],
});
```

Requires the `search` plugin enabled per-entity in your schema. See [Search](/concepts/search).

### `db.useAggregate` — live count / sum / avg / groupBy

```tsx theme={null}
const { data: stats } = db.useAggregate("Order", {
  count: "*",
  sum: ["amount"],
  groupBy: ["status"],
  where: { customerId: "c_xyz" },
});
```

`count` is `"*"` (or a column name for `COUNT(col)`); `sum` / `avg` / `min` / `max` take arrays of column names; `groupBy` takes an array of columns (or date-bucket specs). `data` is one row per group.

## Sessions

```tsx theme={null}
import { useSession } from "@pylonsync/react";
import { db } from "@pylonsync/react";

function NavBar() {
  const { auth, signOut } = useSession(db.sync);
  if (!auth) return <a href="/login">Sign in</a>;
  return (
    <>
      <span>{auth.user.displayName}</span>
      <button onClick={signOut}>Sign out</button>
    </>
  );
}
```

`useSession` returns a live `auth` object that re-renders on sign-in, sign-out, org switch, or remote session revoke. It also exposes `selectOrg`, `clearOrg`, and `refresh` for multi-tenant flows.

## Connection status

```tsx theme={null}
import { useSyncStatus } from "@pylonsync/react";

function ConnectionIndicator() {
  const status = useSyncStatus();
  // "connected" | "connecting" | "reconnecting" | "offline"
  if (status === "connected") return null;
  return <div className="banner">{status}…</div>;
}
```

## Presence + rooms

```tsx theme={null}
import { useRoom } from "@pylonsync/react";

function Editor({ docId, userId }: { docId: string; userId: string }) {
  const room = useRoom(`doc:${docId}`, userId);
  return (
    <>
      {room.peers.map((p) => (
        <Cursor key={p.id} x={p.presence.x} y={p.presence.y} />
      ))}
      <textarea
        onChange={(e) => room.setPresence({ cursor: e.target.selectionStart })}
      />
    </>
  );
}
```

## Multiplayer shards

For tick-driven multiplayer (games, collaborative canvases):

```tsx theme={null}
import { useShard } from "@pylonsync/react";

const { snapshot, send, connected } = useShard<GameState, Input>(
  `match_${matchId}`,
  { subscriberId: userId },
);
```

See [Live queries → shards](/concepts/live-queries#shards).

## Imperative calls

For one-shot reads/writes outside the React tree (effects, event handlers, server-rendered code):

```ts theme={null}
import { db } from "@pylonsync/react";

const id = await db.insert("Todo", { title: "x", done: false });
await db.update("Todo", id, { done: true });
await db.delete("Todo", id);

const result = await db.fn<{ ok: boolean }>("processOrder", { orderId });

for await (const chunk of db.streamFn("chat", { prompt: "hi" })) {
  console.log(chunk);
}
```

These hit the local store optimistically (where applicable) and the server in the background — the same path as the hooks.

## Auth helpers

For sign-in flows that the framework owns end-to-end (cookie + storage update + session refresh), use the [`@pylonsync/next/auth`](/clients/next#6-auth-flows---pylonsyncnextauth) helpers (works outside Next.js too — the auth module has no Next-specific deps despite the package path):

```ts theme={null}
import {
  loginWithPassword,
  signupWithPassword,
  logout,
  startOAuthLogin,
} from "@pylonsync/next/auth";

await loginWithPassword({ email, password });
await logout();
```

For long-running clients, run the session auto-refresher to keep tokens fresh:

```ts theme={null}
import { startSessionAutoRefresh } from "@pylonsync/react";

const cancel = startSessionAutoRefresh({ intervalSeconds: 300 });
// later: cancel()
```

## TypeScript

For end-to-end type safety from schema → hooks, use `createTypedDb` with your manifest's generated types:

```ts theme={null}
import { createTypedDb } from "@pylonsync/react";
import type { Schema } from "./pylon.codegen";

export const typedDb = createTypedDb<Schema>();

// Now `typedDb.useQuery("Todo")` autocompletes the entity name +
// types `data` as `Todo[]` automatically.
```

Run `pylon codegen client --target ts` to emit `Schema` from your `app.ts`.
