React hooks for xNet -- the primary API for building xNet applications.
Status: Mixed Stable root contract:
XNetProvider,useXNet,useQuery,useMutate,useNode,useIdentity,ErrorBoundary,OfflineIndicatorExperimental entrypoints:@xnetjs/react/database,@xnetjs/react/experimentalInternal entrypoint:@xnetjs/react/internal
See docs/reference/api-lifecycle-matrix.md for the current lifecycle table and migration guidance.
pnpm add @xnetjs/react @xnetjs/dataimport { XNetProvider, useQuery, useMutate, useNode } from '@xnetjs/react'
import { MemoryNodeStorageAdapter, defineSchema, text, select } from '@xnetjs/data'
// 1. Define your schema
const TaskSchema = defineSchema({
name: 'Task',
namespace: 'myapp://',
properties: {
title: text({ required: true }),
status: select({
options: [
{ id: 'todo', name: 'To Do' },
{ id: 'done', name: 'Done' }
] as const
})
}
})
// 2. Wrap your app with the provider
function App() {
return (
<XNetProvider
config={{
nodeStorage: new MemoryNodeStorageAdapter(),
authorDID: identity.did,
signingKey: privateKey,
runtime: {
mode: 'worker',
fallback: 'main-thread'
}
}}
>
<TaskApp />
</XNetProvider>
)
}XNetProvider now exposes runtime policy explicitly. Use useXNet().runtimeStatus to inspect the requested mode, active mode, and any visible fallback when bootstrapping web, Electron, or test environments.
flowchart TD
subgraph Core["Core Hooks"]
useQuery["useQuery<br/><small>Read nodes with filters</small>"]
useMutate["useMutate<br/><small>Create, update, delete</small>"]
useNode["useNode<br/><small>Rich text + Y.Doc</small>"]
useIdentity["useIdentity<br/><small>Current user DID</small>"]
useNodeStore["useNodeStore<br/><small>Direct store access</small>"]
end
subgraph Comments["Comment Hooks"]
useComments["useComments"]
useCommentCount["useCommentCount"]
end
subgraph History["History Hooks"]
useHistory["useHistory"]
useUndo["useUndo"]
useAudit["useAudit"]
useDiff["useDiff"]
useBlame["useBlame"]
useVerification["useVerification"]
end
subgraph Hub["Hub Hooks"]
useHubStatus["useHubStatus"]
useBackup["useBackup"]
useFileUpload["useFileUpload"]
useHubSearch["useHubSearch"]
useRemoteSchema["useRemoteSchema"]
usePeerDiscovery["usePeerDiscovery"]
end
subgraph Plugin["Plugin Hooks"]
usePluginRegistry["usePluginRegistry"]
usePlugins["usePlugins"]
useContributions["useContributions"]
useViews["useViews"]
useCommands["useCommands"]
end
Core --> Comments
Core --> History
Core --> Hub
Core --> Plugin
Query nodes with automatic real-time updates.
import { useQuery } from '@xnetjs/react'
function TaskList() {
const { data: tasks, loading, error } = useQuery(TaskSchema)
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
{task.title} {/* Direct property access -- no .properties needed */}
<span>{task.status}</span>
</li>
))}
</ul>
)
}Query by ID:
const { data: task } = useQuery(TaskSchema, taskId)Filtered & Sorted:
const { data: todoTasks } = useQuery(TaskSchema, {
where: { status: 'todo' },
orderBy: { createdAt: 'desc' },
limit: 20
})Create, update, and delete nodes.
import { useMutate } from '@xnetjs/react'
function CreateTaskButton() {
const { create, isPending } = useMutate()
const handleCreate = async () => {
const task = await create(TaskSchema, {
title: 'New Task',
status: 'todo'
})
console.log('Created:', task.id)
}
return (
<button onClick={handleCreate} disabled={isPending}>
{isPending ? 'Creating...' : 'Create Task'}
</button>
)
}Update:
const { update } = useMutate()
await update(TaskSchema, taskId, { status: 'done' }) // Type-checked!Delete:
const { remove } = useMutate()
await remove(taskId) // Soft deleteTransactions (atomic):
const { mutate } = useMutate()
await mutate([
{ type: 'update', id: task1.id, data: { order: 1 } },
{ type: 'update', id: task2.id, data: { order: 2 } },
{ type: 'delete', id: task3.id }
])Load a node with its Y.Doc for collaborative rich text editing.
import { useNode } from '@xnetjs/react'
import { RichTextEditor } from '@xnetjs/editor/react'
const PageSchema = defineSchema({
name: 'Page',
namespace: 'myapp://',
properties: { title: text({ required: true }) },
document: 'yjs'
})
function DocumentEditor({ pageId }) {
const {
data: page, // FlatNode -- page.title works directly
doc, // Y.Doc for rich text
update, // Type-safe property updates
loading,
error,
syncStatus, // 'offline' | 'connecting' | 'connected'
peerCount, // Connected peers
presence // [{ did, name, color, lastSeen, isStale }]
} = useNode(PageSchema, pageId, {
createIfMissing: { title: 'Untitled' },
did: myDid
})
if (loading) return <p>Loading...</p>
if (!page || !doc) return <p>Not found</p>
return (
<div>
<input value={page.title} onChange={(e) => update({ title: e.target.value })} />
<RichTextEditor ydoc={doc} />
</div>
)
}const { threads, addComment, resolveThread } = useComments({ nodeId })
const count = useCommentCount(nodeId)const { timeline, materializeAt, diff } = useHistory(nodeId)
const { undo, redo, canUndo, canRedo } = useUndo(nodeId)
const { entries, activity } = useAudit(nodeId)
const { diff: runDiff, result } = useDiff(nodeId)
const { blame } = useBlame(nodeId)
const { verify, quickCheck } = useVerification(nodeId)const status = useHubStatus()
const { upload: uploadBackup, download: downloadBackup } = useBackup()
const { upload, uploading, progress } = useFileUpload()
const { search, results } = useHubSearch()
const { schema } = useRemoteSchema(schemaId)
const { peers } = usePeerDiscovery()const { registry } = usePluginRegistry()
const { plugins } = usePlugins()
const { contributions } = useContributions('view')
const { views } = useViews()
const { commands, execute } = useCommands()The package includes a full sync infrastructure layer:
| Module | Description |
|---|---|
WebSocketSyncProvider |
WebSocket-based sync with hub |
SyncManager |
Orchestrates sync across providers |
NodePool |
Manages Y.Doc instances |
ConnectionManager |
WebSocket connection lifecycle |
NodeStoreSyncProvider |
Syncs NodeStore changes |
MetaBridge |
Bridges node metadata to Yjs |
OfflineQueue |
Queues mutations while offline |
BlobSync |
Syncs file blobs to hub |
All hooks return FlatNode<Schema> which flattens properties to the top level:
// Properties are directly accessible
const title = page.title // string (correctly typed)| Parameter | Type | Description |
|---|---|---|
schema |
DefinedSchema<P> |
The schema to query |
idOrFilter? |
string | QueryFilter |
Node ID or filter options |
Returns: create, update, remove, restore, mutate, isPending, pendingCount
| Parameter | Type | Description |
|---|---|---|
schema |
DefinedSchema<P> |
Schema with document: 'yjs' |
id |
string | null |
Node ID |
options? |
UseNodeOptions |
Configuration |
@xnetjs/data-- Schema system and NodeStore@xnetjs/editor-- Rich text editor components@xnetjs/history-- Time machine and audit hooks@xnetjs/plugins-- Plugin system hooks@xnetjs/identity-- DID and key management