syncore-react-realtime
Syncore React Realtime
Use this skill when wiring Syncore into React apps or debugging hook inference, watch lifecycle, or result propagation.
Documentation Sources
Read these first:
packages/react/src/index.tsxpackages/react/AGENTS.mdpackages/platform-web/src/react.tsxpackages/platform-node/src/ipc-react.tsxpackages/platform-expo/src/react.tsxpackages/next/src/index.tsxdocs/quickstarts/react-web.mddocs/quickstarts/expo.mddocs/quickstarts/electron.mddocs/quickstarts/next-pwa.mdexamples/electron/src/renderer/App.tsxexamples/expo/App.tsxexamples/next-pwa/app/bookmarks-screen.tsx
Instructions
Provider First
Every hook depends on SyncoreProvider or a platform wrapper that mounts it
for you.
Raw provider:
import { SyncoreProvider } from "syncorejs/react";
<SyncoreProvider client={client}>{children}</SyncoreProvider>;
Common wrapper providers in app code are:
SyncoreBrowserProviderfromsyncorejs/browser/reactSyncoreElectronProviderfromsyncorejs/node/ipc/reactSyncoreExpoProviderfromsyncorejs/expo/reactSyncoreNextProviderfromsyncorejs/next
If no provider is present, hooks will throw.
useQuery
useQuery is the core reactive read API. It returns undefined while the
first result is still loading.
import { useQuery } from "syncorejs/react";
import { api } from "../syncore/_generated/api";
function Tasks() {
const tasks = useQuery(api.tasks.list) ?? [];
return <pre>{JSON.stringify(tasks, null, 2)}</pre>;
}
Pass "skip" as the second argument to suppress the subscription entirely:
import { skip, useQuery } from "syncorejs/react";
const results = useQuery(
api.notes.search,
searchText.trim() ? { query: searchText.trim() } : skip
);
useMutation And useAction
Mutations and actions return callable functions with typed args and results inferred from the reference.
import { useAction, useMutation } from "syncorejs/react";
import { api } from "../syncore/_generated/api";
const createTask = useMutation(api.tasks.create);
const exportTasks = useAction(api.tasks.exportTasks);
Prefer Inference Over Manual Generics
Preferred:
const todos = useQuery(api.todos.list) ?? [];
const createTodo = useMutation(api.todos.create);
Fallbacks with manual generics should be treated as a signal to inspect shared typing, not as the ideal pattern.
Watch Lifecycle Matters
Syncore React hooks are thin wrappers over SyncoreClient.watchQuery(...).
When changing React bindings:
- preserve stable args behavior
- dispose watches when refs or args change
- keep React types aligned with
SyncoreClient
useQueries
Use useQueries when a keyed batch of query subscriptions is the right shape
for the view:
const data = useQueries([
{ key: "tasks", reference: api.tasks.list },
{ key: "notes", reference: api.notes.list }
]);
Represent the result as keyed query state, not as a substitute for ordinary component composition.
useQueries is intentionally less strongly typed than useQuery,
useMutation, and useAction. Prefer the single-hook APIs when they fit the
component shape better.
Examples
Basic App Wiring
import { SyncoreProvider, useMutation, useQuery } from "syncorejs/react";
import type { SyncoreClient } from "syncorejs";
import { api } from "../syncore/_generated/api";
export function App({ client }: { client: SyncoreClient }) {
return (
<SyncoreProvider client={client}>
<Todos />
</SyncoreProvider>
);
}
function Todos() {
const todos = useQuery(api.todos.list) ?? [];
const createTodo = useMutation(api.todos.create);
return (
<div>
<button onClick={() => void createTodo({ title: "Ship local-first DX" })}>
Add
</button>
{todos.map((todo) => (
<div key={todo._id}>{todo.title}</div>
))}
</div>
);
}
Platform Wrapper Example
import { SyncoreNextProvider } from "syncorejs/next";
const createWorker = () =>
new Worker(new URL("./syncore.worker", import.meta.url), {
type: "module"
});
export function AppSyncoreProvider({
children
}: {
children: React.ReactNode;
}) {
return (
<SyncoreNextProvider createWorker={createWorker}>
{children}
</SyncoreNextProvider>
);
}
Loading State
function Notes() {
const notes = useQuery(api.notes.list);
if (notes === undefined) {
return <div>Loading...</div>;
}
return <pre>{JSON.stringify(notes, null, 2)}</pre>;
}
Best Practices
- Always mount hooks under
SyncoreProvideror a platform wrapper provider - Prefer
useQuery(api.foo.bar)without manual generics when typing supports it - Handle
undefinedloading state explicitly - Use
skipinstead of hand-rolled conditional subscriptions - Keep hooks thin over
SyncoreClientrather than duplicating runtime behavior - Prefer platform wrapper providers in app shells and the raw provider in low-level tests or custom integrations
- When editing hook types, validate inference and watch cleanup together
Common Pitfalls
- Calling hooks outside a Syncore provider
- Treating manual generics as the desired steady state for app code
- Forgetting that query subscriptions must be cleaned up when args or refs change
- Narrowing React-facing types more than the core client allows
- Using
useQuerieswhere better-typed independent hooks would be clearer
References
packages/react/src/index.tsxpackages/react/AGENTS.mdpackages/platform-web/src/react.tsxpackages/platform-node/src/ipc-react.tsxpackages/platform-expo/src/react.tsxpackages/next/src/index.tsx