@doync/react
v0.4.0
Published
doync React adapter: thin useQuery / useQueryOnce / useLocalQuery / useMutation hooks over the client call surface
Downloads
1,199
Maintainers
Readme
@doync/react
Thin React hooks over the doync client call surface (subscribe / once / local / mutate). The hooks consume any DoyncClient — construct one with @doync/web or @doync/mobile and pass it to the Provider. Environment packages are never re-exported from here.
Apps import client types (DoyncClient, View, …) from this package; @doync/client is for adapter authors.
Install
pnpm add @doync/react @doync/client @doync/core
# plus one environment package:
pnpm add @doync/web # browser
# or
pnpm add @doync/mobile # React NativePeer: react ≥ 18.
Provider
Boot the environment client once, then provide it to the tree. Keep the same client object for the life of the app (or tab); drive login/logout through client.updateAuth, not by reconstructing the client.
import { DoyncProvider } from '@doync/react'
import { createWebClient } from '@doync/web'
// or: import { createMobileClient } from '@doync/mobile'
const client = createWebClient({/* worker, authData, … — see @doync/web */})
export function App() {
return (
<DoyncProvider client={client}>
<TaskList />
</DoyncProvider>
)
}useDoyncClient() returns the same client (throws outside a Provider) when you need the imperative surface from a component.
Shared definition module
Hooks take the same queries / mutations trees every client uses — define them once through createDoync / createDoyncDrizzle and import that module from web, mobile, and the DB-worker entry:
// shared/data.ts — one data layer for every client
export { schema, queries, mutations }Query-taking hooks accept a Bound query: call the registered leaf with its args, queries.issues.open(args). Binding is pure; a fresh Bound query object per render does not bust memoization (key is name + args). Falsy (cond && queries.issues.open(args)) means "no query" and keeps stable hook order.
useQuery
Subscribe to a registered query. Mount retains the shared View handle; unmount releases it. Rows update live as pokes and optimistic writes move them. The second tuple element is the View status.
import { useQuery } from '@doync/react'
import { queries } from './shared/data'
function IssueList({ projectId }: { projectId: string }) {
const [issues, status] = useQuery(queries.issues.open({ projectId }))
// multi-row: issues is readonly Row[]
// a sql.one / findFirst query types as Row | undefined instead
if (status.status !== 'complete' && issues.length === 0) {
return <Spinner />
}
return (
<ul>
{issues.map((issue) => (
<li key={issue.id}>{issue.title}</li>
))}
</ul>
)
}Options (second argument):
ttl— connected-clock grace for this Subscription.skip: true— short-circuit the desire while keeping shape-preserving empties ([]multi-row /undefinedone-query). Distinct from a falsy query argument, which yieldsundefinedrows and status'unknown'for both shapes.
One-ness is inferred from the bound query (sql.one / Drizzle findFirst) — never passed as an option.
useQueryOnce
Cache-and-network Once read. Rows render from the local Replica immediately, then update when the server answer lands. status tracks the network half.
const [rows, { status }] = useQueryOnce(queries.issueById({ id }))
// status: 'loading' | 'success' | 'error' | 'skipped' (falsy argument)useLocalQuery
Arbitrary SQL over the synced Replica — aggregates, joins, window functions. Re-runs on local commits; never registered upstream; offline-capable and free to the server.
const [rows, status] = useLocalQuery<{ n: number }>(
'select count(*) as n from issue where open = 1',
)
// or: useLocalQuery({ sql: 'select … where id = ?', params: [id] })
// or: useLocalQuery(() => ({ sql, params }))A falsy source skips (undefined rows, status 'unknown').
useMutation
A registered mutation as a callable. Apply optimistically and push; render off client, await authoritative confirmation off server when it matters.
import { useMutation } from '@doync/react'
import { mutations } from './shared/data'
function CreateIssue() {
const createIssue = useMutation(mutations.issue.create)
async function onSubmit(input: { id: string; title: string }) {
const { client, server } = createIssue(input)
await client // local apply settled (throws if the local body rejects)
// optional: await server // Mirror confirmed (throws if rejected)
}
return /* … */
}Connection and schema status
import { useConnectionStatus, useSchemaStatus } from '@doync/react'
function StatusPill() {
const connection = useConnectionStatus()
// 'connecting' | 'connected' | 'disconnected' | 'error' | 'needs-auth'
const schema = useSchemaStatus()
// null when nominal; else { kind: 'reload' | 'server-behind' | 'resync' | 'forget', message }
// Re-reads on both entry AND silent clear — a banner driven only by a
// one-shot callback would stick after recovery.
return (
<>
<span>{connection}</span>
{schema ? <Banner>{schema.message}</Banner> : null}
</>
)
}Public surface
| Export | Role |
| --- | --- |
| DoyncProvider / DoyncProviderProps | Provide the client |
| useDoyncClient | Imperative client from context |
| useQuery / UseQueryOptions | Live Subscription |
| useQueryOnce / OnceStatus | Cache-and-network Once |
| useLocalQuery / LocalSource | Local SQL over the Replica |
| useMutation | Optimistic mutate + push |
| useConnectionStatus | Mirror socket health |
| useSchemaStatus | Schema-handling state (or null) |
| BoundQuery | Re-exported from @doync/core |
| DoyncClient / View / OnceView / ViewStatus / QueryStatus / ConnectionStatus / SchemaEvent / SchemaEventKind / FalsyQuery / MutationOptions / MutationResult / LogoutBehavior / SubscribeOptions / PreloadOptions / PreloadHandle / WarmupOptions / WarmupHandle | Client call-surface family (re-exported from @doync/client) |
Internal API
The main entry (.) is the semver-governed public surface documented here. Anything imported from @doync/react/internal may change in any release, including patches, without notice — use it only if you accept that risk.
