Mutate data

Writes go through useMutation from @tinycld/core/lib/mutations - not directly from @tanstack/react-query. The wrapper accepts a generator function as mutationFn and awaits each yielded pbtsdb Transaction automatically. You describe what should change in the order it should change; the wrapper handles the optimistic update, persistence, and rollback-on-failure machinery.

Before you mutate

Your screen needs a collection handle (from useStore) and, usually, a form. See Forms for the standard react-hook-form + zod setup this page pairs with.

The pattern

import { useStore } from '@tinycld/core/lib/pocketbase'
import { useMutation } from '@tinycld/core/lib/mutations'
import { handleMutationErrorsWithForm } from '@tinycld/core/lib/errors'
import { newRecordId } from 'pbtsdb'
import { useRouter } from 'expo-router'
import { useForm } from 'react-hook-form'

export default function NewExample() {
    const router = useRouter()
    const [exampleCollection] = useStore('example')
    const { handleSubmit, setError, getValues } = useForm<FormData>()

    const create = useMutation({
        mutationFn: function* (data: FormData) {
            yield exampleCollection.insert({ id: newRecordId(), ...data })
        },
        onSuccess: () => router.back(),
        onError: handleMutationErrorsWithForm({ setError, getValues }),
    })

    return <ExampleForm onSubmit={handleSubmit(create.mutate)} />
}

Yielded transactions

Every pbtsdb collection method (insert, update, delete) returns a Transaction. When you yield one inside the generator, core awaits its isPersisted promise before advancing to the next statement. You write the code as if it were synchronous:

mutationFn: function* (data: FormData) {
    yield exampleCollection.insert({ id: newRecordId(), ...data })
    yield tagsCollection.update(data.tagId, { last_used: new Date().toISOString() })
}

Sequential writes above; parallel writes by yielding an array:

mutationFn: function* (data: FormData) {
    yield [
        exampleCollection.insert({ id: newRecordId(), ...data }),
        auditCollection.insert({ id: newRecordId(), action: 'create' }),
    ]
}

Core awaits all transactions in the array in parallel, then advances.

Outside of a component

If you need to perform a mutation from a plain async function (a seed script, an event handler outside the render tree), use performMutations:

import { performMutations } from '@tinycld/core/lib/mutations'

await performMutations(function* () {
    yield exampleCollection.insert({ id: newRecordId(), title: 'Seed' })
})

Same generator semantics; no React involvement.

Error handling

handleMutationErrorsWithForm maps PocketBase field-level validation errors back onto the matching react-hook-form fields via setError. Form-wide errors (network failures, permission denials) become a root-level error you can render with <FormErrorSummary />. Together they cover the normal shape of a form submit - you usually don’t need a custom onError.

For non-form mutations, pass a plain onError: (err) => captureException(err) and handle the display however the surface needs.

Common mistakes

For the form side of this pattern, see Forms.