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

Open the database once at app startup and wrap your component tree with TalaDBProvider:

tsx
// main.tsx (or app/_layout.tsx in Expo)
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.


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>

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.