Skip to content

useMutate

import { useMutate } from '@xnet/react'
import { TaskSchema } from './schema'
function AddTaskButton() {
const { create, isPending } = useMutate()
return (
<button
disabled={isPending}
onClick={() =>
create(TaskSchema, {
title: 'New task',
status: 'todo'
})
}
>
{isPending ? 'Creating...' : 'Add Task'}
</button>
)
}
function useMutate(): UseMutateResult

Takes no parameters. Returns methods for all mutation operations.

MethodSignatureDescription
create(schema, data, id?, options?) => Promise<FlatNode | null>Create a new node. Returns the created node or null if store isn’t ready.
update(schema, id, data, options?) => Promise<FlatNode | null>Update properties. Only changed fields are written.
remove(id, options?) => Promise<void>Soft-delete a node. Recoverable via restore.
restore(id, options?) => Promise<FlatNode | null>Restore a soft-deleted node.
mutate(ops[], options?) => Promise<TransactionResult | null>Batch multiple operations atomically.
FieldTypeDescription
isPendingbooleantrue if any mutation is currently in flight.
pendingCountnumberNumber of concurrent mutations in flight.
FieldTypeDefaultDescription
optimisticbooleantrueWhether to apply optimistic updates. Currently accepted but unused — all writes go directly to the store.
const { create, update, remove, restore } = useMutate()
// Create
const task = await create(TaskSchema, {
title: 'Ship v1',
status: 'todo'
})
// Update (sparse — only changed fields)
await update(TaskSchema, task.id, { status: 'doing' })
// Soft-delete
await remove(task.id)
// Restore
await restore(task.id)

Pass an optional third argument to create to specify the node ID:

const task = await create(TaskSchema, { title: 'Known ID' }, 'my-custom-id')
// task.id === 'my-custom-id'

Use mutate() to execute multiple operations atomically:

const { mutate } = useMutate()
const result = await mutate([
{ type: 'create', schema: TaskSchema, data: { title: 'Task A' } },
{ type: 'create', schema: TaskSchema, data: { title: 'Task B' } },
{ type: 'update', id: existingId, data: { status: 'done' } },
{ type: 'delete', id: oldTaskId }
])

When creating related nodes in a transaction, the result includes a tempIds map from your provided IDs to the generated real IDs:

const result = await mutate([
{
type: 'create',
schema: ProjectSchema,
data: { title: 'New Project' },
id: 'temp-project'
},
{
type: 'create',
schema: TaskSchema,
data: {
title: 'First task',
projectId: 'temp-project' // Reference the temp ID
}
}
])
// result.tempIds['temp-project'] → real generated ID
interface TransactionResult {
batchId: string // Unique batch identifier
results: (NodeState | null)[] // One result per operation (null for deletes)
changes: NodeChange[] // All changes produced
tempIds: Record<string, string> // Temp ID → real ID mapping
}
function TaskForm({ taskId }: { taskId: string }) {
const { data: task } = useQuery(TaskSchema, taskId)
const { update } = useMutate()
const [title, setTitle] = useState('')
useEffect(() => {
if (task) setTitle(task.title)
}, [task])
const save = () => update(TaskSchema, taskId, { title })
return (
<form
onSubmit={(e) => {
e.preventDefault()
save()
}}
>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit">Save</button>
</form>
)
}
const { create, isPending, pendingCount } = useMutate()
// Disable UI during mutations
<button disabled={isPending}>Save</button>
// Show count for batch operations
{pendingCount > 0 && <span>{pendingCount} operations pending...</span>}
  • useQuery — read the data you mutate
  • useNode — alternative for single-node editing with Yjs
  • defineSchema — define the schemas you write to