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
- Importing
useMutationfrom@tanstack/react-query. The upstream hook doesn’t understand generator functions - it will call yours once, get back aGeneratorobject, and immediately report success. Always import from@tinycld/core/lib/mutations. - Forgetting
newRecordId(). pbtsdb requires you to assign the id client-side so optimistic updates can reference it. PocketBase accepts any 15-char alphanumeric id. - Using
awaitinside the generator.yielda transaction, don’tawaitit. The generator shape is what makes the await implicit - awaiting explicitly double-resolves the promise.
For the form side of this pattern, see Forms.