React Hooks (@taladb/react)
@taladb/react is the official hooks package for TalaDB. It wraps TalaDB's live query API with useSyncExternalStore so your components automatically re-render whenever the underlying data changes — no manual subscriptions, no boilerplate.
Works in React (browser + Node.js) and React Native with the same API.
Installation
pnpm add taladb @taladb/web @taladb/reactpnpm add taladb @taladb/react-native @taladb/reactRequirements: React 18+ · taladb 0.4+
Setup
Two ways to provide the database. Name form (since v0.9.0) — the provider owns the openDB lifecycle: it opens lazily on the client, renders fallback until ready (so hooks never see a missing db), and closes on unmount. The right default for browser apps and required for Next.js, where openDB cannot run during server rendering:
import { TalaDBProvider } from '@taladb/react'
root.render(
<TalaDBProvider name="myapp.db" fallback={<Splash />}>
<App />
</TalaDBProvider>
)Instance form — you open the database yourself and hand it in (plain React with top-level await, React Native):
import { openDB } from 'taladb'
import { TalaDBProvider } from '@taladb/react'
const db = await openDB('myapp.db')
root.render(
<TalaDBProvider db={db}>
<App />
</TalaDBProvider>
)React Native: call
TalaDBModule.initialize('myapp.db')beforeopenDB. See the React Native guide for setup details.Next.js: the package ships the
'use client'directive, so importing hooks never trips the RSC boundary. Note that nothing beneath the provider is server-rendered — it showsfallbackuntil the database opens on the client — so do not wrap the root layout or a page that has to be indexed. See Next.js setup for provider placement, CSP, and bundler configuration.
Registering collection options
Hooks resolve their collection by name, so they need to know how that collection is configured. Register each collection's options — schema, syncSchema, migrateDocument — once on the provider:
<TalaDBProvider
name="myapp.db"
fallback={<Splash />}
collections={{
bookings: { schema: Booking, syncSchema: { version: 1, required: ['listingId'] } },
reviews: { schema: Review, syncSchema: { version: 1 } },
}}
>
<App />
</TalaDBProvider>Every hook below it — useCollection, useFind, useFindOne, useAggregate, useWrite — now opens a configured collection: a local write hard-fails validation, and the engine stamps the document's _v shape version. useCollection(name, options) overrides the registry for a single call.
Changed in 0.9.3
Before 0.9.3 there was no way to give the hooks these options: useCollection(name) called db.collection(name) bare, so a write through useWrite silently skipped the strict schema and the _v stamp that db.collection(name, { … }) applies. An app that followed this guide lost local validation without any warning. Register collections and that gap is closed.
Quick example
import { useCollection, useFind } from '@taladb/react'
interface Note {
_id?: string
text: string
pinned: boolean
}
export function NoteList() {
const notes = useCollection<Note>('notes')
const { data, loading } = useFind(notes, { pinned: true })
if (loading) return <p>Loading…</p>
return (
<ul>
{data.map((note) => (
<li key={note._id}>{note.text}</li>
))}
</ul>
)
}The component re-renders automatically whenever a pinned note is inserted, updated, or deleted — no useEffect, no manual subscribe calls.
Hooks
useCollection
Returns a stable, memoised Collection<T> from the nearest <TalaDBProvider>. Use this as the collection argument to useFind and useFindOne.
const collection = useCollection<T>(name: string): Collection<T>The same Collection object is returned on every render (memoised by db identity + name), so you can pass it directly to useFind without wrapping it in useMemo.
const articles = useCollection<Article>('articles')useFind
Subscribes to a live query and returns all matching documents. Re-renders whenever the result set changes.
const { data, loading } = useFind<T>(
collection: Collection<T>,
filter?: Filter<T>,
): { data: T[]; loading: boolean }data | Array of matching documents. Empty array while loading is true. |
loading | true until the first snapshot is delivered from the database. |
Inline filter objects are safe — the filter is serialised to a string internally, so { active: true } written directly in JSX does not cause a re-subscription on every render.
// All documents
const { data: all } = useFind(users)
// With filter
const { data: active } = useFind(users, { active: true })
// With comparison operators
const { data: recent } = useFind(articles, {
publishedAt: { $gte: Date.now() - 86_400_000 },
})See Filters for the full filter DSL.
useFindOne
Subscribes to a single document. Returns the first match, or null if nothing matches.
const { data, loading } = useFindOne<T>(
collection: Collection<T>,
filter: Filter<T>,
): { data: T | null; loading: boolean }interface User {
_id?: string
name: string
role: 'admin' | 'member'
}
const users = useCollection<User>('users')
const { data: user, loading } = useFindOne(users, { _id: userId })
if (loading) return <Spinner />
if (!user) return <p>User not found.</p>
return <p>Hello, {user.name}</p>useWrite
Writes to a collection. The write is local, immediate, durable, and reactive — every useFind / useFindOne / useAggregate subscribed to that collection re-renders once it commits.
const { write, writeAsync, pending, error } = useWrite<T>({
collection: string,
})const { write, pending } = useWrite<Order>({ collection: 'orders' })
write({ type: 'insert', doc: { sku: 'A1', status: 'new' } })
write({ type: 'update', where: { _id }, set: { status: 'shipped' } })
write({ type: 'delete', where: { _id } })write is fire-and-forget — failures land on error rather than throwing into render. writeAsync returns a promise you can await and catch.
There is no network step here and no rollback to reason about: the database is on the device, so the write either committed or threw. If the change webhook is enabled (openDB({ webhook })), its HTTP request is dispatched after the commit, outside this hook and outside pending — a webhook is a notification, not part of the write's success.
Why not useMutation?
Because this hook is exactly a local write. The React Query-shaped name carries an expectation of a network round-trip that it would borrow and then fail to meet — so it belongs to @taladb/react/query, which does perform one. Renamed from useMutation in 0.11.0.
useTalaDB
Returns the raw TalaDB instance from context. Use this when you need direct access to db.collection() for write operations outside of useCollection.
const db = useTalaDB(): TalaDBfunction AddNoteButton() {
const db = useTalaDB()
async function handleClick() {
await db.collection('notes').insert({ text: 'New note', pinned: false })
// useFind subscribers update automatically — no setState needed
}
return <button onClick={handleClick}>Add Note</button>
}Full example
A complete notes app with live queries, inserts, and deletes:
import { useCollection, useFind, useTalaDB } from '@taladb/react'
interface Note {
_id?: string
text: string
createdAt: number
}
export function NotesApp() {
const db = useTalaDB()
const notes = useCollection<Note>('notes')
const { data, loading } = useFind(notes)
async function addNote() {
await notes.insert({ text: `Note ${Date.now()}`, createdAt: Date.now() })
}
async function deleteNote(id: string) {
await notes.deleteOne({ _id: id })
}
return (
<div>
<button onClick={addNote}>Add Note</button>
{loading && <p>Loading…</p>}
<ul>
{data.map((note) => (
<li key={note._id}>
{note.text}
<button onClick={() => deleteNote(note._id!)}>Delete</button>
</li>
))}
</ul>
</div>
)
}React Native
@taladb/react works in React Native without any changes. Install @taladb/react-native as the platform adapter and the same hooks work on iOS and Android:
// App.tsx
import { TalaDBModule } from '@taladb/react-native'
import { openDB } from 'taladb'
import { TalaDBProvider } from '@taladb/react'
export default function App() {
const [db, setDb] = useState<TalaDB | null>(null)
useEffect(() => {
async function init() {
await TalaDBModule.initialize('myapp.db')
setDb(await openDB('myapp.db'))
}
init()
}, [])
if (!db) return null
return (
<TalaDBProvider db={db}>
<NoteList />
</TalaDBProvider>
)
}Everything else — useCollection, useFind, useFindOne — is identical to the browser.
How live queries work
Under the hood, useFind and useFindOne call collection.subscribe(filter, callback) and bridge it to React with useSyncExternalStore. This gives you:
- Zero-tearing — all components reading from the same collection see the same snapshot within a single render pass (concurrent React guarantee)
- Automatic cleanup — the subscription is cancelled when the component unmounts
- Filter stability — the filter is serialised to JSON for subscription identity, so inline objects like
{ active: true }don't trigger re-subscription on every render
The subscription polls for changes every 300 ms on all platforms, with an additional BroadcastChannel nudge in the browser so cross-tab writes propagate immediately.
TypeScript tips
Pass your document type as a generic to get fully typed data:
interface Article {
_id?: string
title: string
locale: string
publishedAt: number
}
const articles = useCollection<Article>('articles')
const { data } = useFind(articles, { locale: 'en' })
// ^? Article[]Filter and update types are inferred from the document type — typos on field names are caught at compile time.
Talking to a server
The hooks above are local-only: they read and write the database on the device and never touch the network. To hydrate a collection from your API and queue writes back to it, see Local-First Data (@taladb/react/query) — useQuery and useMutation in the React Query shape, with the local collection as the cache.