Skip to main content
@pylonsync/react is the React-first client. It wraps @pylonsync/sync with useSyncExternalStore-backed hooks so your components re-render automatically when entity data changes.

Install

bun add @pylonsync/sdk @pylonsync/react
The React package depends on @pylonsync/sync transitively; you don’t need to install it directly.

Configure once

In your app entry:
import { configureClient } from "@pylonsync/react";

configureClient({
  baseUrl: "https://your-app.com",
  appName: "default", // namespaces localStorage keys
});
Now every hook + auth helper uses this base URL. The auth token is persisted to localStorage under pylon_token (or pylon:<appName>:token if you set a custom app name).

Hooks

useQuery

Live-updating list of entity rows. Re-renders when the local store changes (after server pushes, optimistic mutations, or hydration).
import { useQuery } from "@pylonsync/react";

function TodoList() {
  const { data: todos, loading, error } = useQuery("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:
const { data } = useQuery("Todo", {
  where: { authorId: "u_xyz", done: false },
  orderBy: { createdAt: "desc" },
  limit: 50,
});

useQueryOne

Single row by id:
const { data: post } = useQueryOne("Post", postId);

useInfiniteQuery

Cursor-paginated list with loadMore:
const { data, hasMore, loadMore, loading } = useInfiniteQuery("Post", { pageSize: 20 });

return (
  <>
    {data.map(p => <PostCard key={p.id} post={p} />)}
    {hasMore && <button onClick={loadMore} disabled={loading}>Load more</button>}
  </>
);

useMutation

Call a server function with typed args:
const { mutate, loading, error } = useMutation("createTodo");

return (
  <button
    onClick={() => mutate({ title: "ship it", authorId: userId })}
    disabled={loading}
  >
    Add
  </button>
);
For optimistic mutations on entities (insert/update/delete with rollback on failure), use useEntityMutation:
const { insert, update, remove } = useEntityMutation("Todo");

await insert({ title: "x", done: false });
await update(todoId, { done: true });
await remove(todoId);
These hit the local store immediately; the sync engine queues the server write and reconciles on response.

useSession

Live-updating auth context:
const session = useSession();
if (!session.userId) return <SignInForm />;
return <Dashboard userId={session.userId} role={session.roles[0]} />;
session mirrors AuthContext and re-renders on sign-in / sign-out / tenant switch.

useSearch

Full-text search with live facet counts (requires the search plugin enabled per-entity):
const { hits, facetCounts, loading } = useSearch("Post", {
  query: input,
  filters: { tags: selectedTag },
  facets: ["tags", "authorId"],
});

useAggregate

Live-updating count/sum/avg/min/max/groupBy:
const { data: stats } = useAggregate("Order", {
  count: true,
  sum: { amount: "total" },
  groupBy: "status",
  where: { customerId: "c_xyz" },
});

useShard

Connect to a tick-driven multiplayer shard:
const { snapshot, send, connected } = useShard<GameState, Input>("match_42", {
  subscriberId: userId,
});

return (
  <>
    {snapshot && <GameView state={snapshot} />}
    <button onClick={() => send({ action: "move", x, y })}>Move</button>
  </>
);
See Live queries for shard concepts.

Auth helpers

Standalone async functions for sign-in flows — they update the storage adapter and then any useSession consumers re-render:
import {
  startMagicLink,
  verifyMagicLink,
  signInWithPassword,
  signInWithGoogle,
  signOut,
  getCurrentSession,
} from "@pylonsync/react";

await startMagicLink("alice@example.com");
const session = await verifyMagicLink("alice@example.com", code);
// → { token, user_id, expires_at }

await signOut();
For long-running clients, start auto-refresh:
import { startSessionAutoRefresh } from "@pylonsync/react";

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

Direct (non-synced) calls

For one-shot fetches that bypass the sync engine (e.g. server-rendered pages, scripts):
import { fetchList, fetchById, insert, update, remove, callFn } from "@pylonsync/react";

const todos = await fetchList("Todo");
const todo  = await fetchById("Todo", id);
const created = await insert("Todo", { title: "x", done: false });
await update("Todo", id, { done: true });
await remove("Todo", id);

const result = await callFn("processOrder", { orderId });
These don’t update the local store — use useEntityMutation instead if you want optimistic UI.

Streaming functions

For functions that stream output (AI chat, live data):
import { streamFn } from "@pylonsync/react";

for await (const chunk of streamFn("chat", { prompt: "tell me a joke" })) {
  console.log(chunk);
}

Files

import { uploadFile, uploadFileMultipart } from "@pylonsync/react";

const result = await uploadFile(file, { filename: "avatar.png" });
// → { id: "file_xyz", url: "/api/files/file_xyz", size: 12345 }

Typed client (codegen)

After pylon codegen client manifest.json --out client.ts, you get a typed wrapper:
import { createTypedDb } from "@pylonsync/react";
import type { AppSchema } from "./client";

const db = createTypedDb<AppSchema>();

// Now everything is typed:
const { data: todos } = db.useQuery("Todo");        // todos: Todo[]
const { mutate } = db.useMutation("createTodo");    // args: CreateTodoInput
The compiler catches typos in entity names, mismatched arg shapes, and wrong field references.

SSR / hydration

For Next.js or any SSR framework, fetch on the server and hydrate on the client:
// On the server
import { getServerData } from "@pylonsync/sync";

const hydration = await getServerData("https://your-app.com", ["Todo", "User"], {
  token: req.cookies.pylon_token,
});

// Pass `hydration` to the client...
// On the client
import { hydrateClient } from "@pylonsync/react";

useEffect(() => {
  hydrateClient(hydration);
}, [hydration]);
The first paint shows server-rendered content; the sync engine then takes over for live updates without a redundant initial pull. For richer Next.js integration (Server Actions, RSC) see @pylonsync/next.

React Native

@pylonsync/react’s hooks work in React Native too, but the storage adapter (localStorage) doesn’t. Use @pylonsync/react-native instead — it has the AsyncStorage bridge and SQLite-backed persistence.

Performance

  • Hooks use useSyncExternalStore so React batches re-renders correctly.
  • The local store keeps an in-memory snapshot per entity; useQuery reads are O(1).
  • Optimistic mutations fire a single re-render across all subscribed components.
  • WebSocket updates batch into one render per server tick to avoid render thrashing.

Common pitfalls

  • Forgetting to call configureClient before mounting — hooks throw if no base URL is set.
  • Using useQuery outside a sync engine — without <PylonProvider> (or the implicit default), the hook can’t subscribe. Add the provider once at the app root.
  • Holding stale data in where filtersuseQuery re-runs the filter on every store notify, so changing where triggers a re-fetch correctly. But if you derive where from React state, memoize it with useMemo to avoid object identity churn.