Skip to content

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 ​

bash
pnpm add taladb @taladb/web @taladb/react
bash
pnpm add taladb @taladb/react-native @taladb/react

Requirements: 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:

tsx
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):

tsx
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') before openDB. 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 shows fallback until 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:

tsx
<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 ​

tsx
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.

ts
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.

tsx
const articles = useCollection<Article>('articles')

useFind ​

Subscribes to a live query and returns all matching documents. Re-renders whenever the result set changes.

ts
const { data, loading } = useFind<T>(
  collection: Collection<T>,
  filter?: Filter<T>,
): { data: T[]; loading: boolean }
dataArray of matching documents. Empty array while loading is true.
loadingtrue 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.

tsx
// 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.

ts
const { data, loading } = useFindOne<T>(
  collection: Collection<T>,
  filter: Filter<T>,
): { data: T | null; loading: boolean }
tsx
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.

ts
const { write, writeAsync, pending, error } = useWrite<T>({
  collection: string,
})
tsx
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.

ts
const db = useTalaDB(): TalaDB
tsx
function 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:

tsx
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:

tsx
// 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:

ts
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.