Connecting Components
Hypertill DB 0.0.4 ships the React helpers you need for day-to-day app work:
DatabaseProvideruseDatabasehooks(auto-generated query hooks)withObservables
The current recommended split is simple:
- use
DatabaseProvideronce at the app root - use
hooksfor reactive reads - use
useDatabasefor writes, screen actions, and imperative work - use
withObservableswhen you need custom reactive composition or live counts
Start with the provider
Wrap your app once:
import { DatabaseProvider } from '@hypertill/db/react'
import { database } from './db'
import { LibraryScreen } from './LibraryScreen'
export default function App() {
return (
<DatabaseProvider database={database}>
<LibraryScreen />
</DatabaseProvider>
)
}
Every reactive component and screen action then works off the same database instance.
Reactive reads with hooks
For each model, you get:
hooks.use<Model>(id)for a single recordhooks.use<Models>(filters?)for listshooks.use<Models>Advanced({ q, clauses, inputs, observeWithColumns })for directQclauses and advanced observation control
Hook names come from your model class names, so Book becomes hooks.useBook() and hooks.useBooks().
Example usage:
import { hooks } from '@hypertill/db/react'
const { data: books, loading } = hooks.useBooks({
search: 'deep',
timeframe: '30d',
sort: 'updated_desc',
})
const { data: book } = hooks.useBook(bookId)
const { data: readingBooks } = hooks.useBooksAdvanced({
inputs: ['reading'],
q: (Q) => [Q.where('status', 'reading')],
})
const { data: orderedChapters } = hooks.useChaptersAdvanced({
inputs: [bookId],
q: (Q) => [Q.where('book_id', bookId), Q.sortBy('position', Q.asc)],
observeWithColumns: ['position'],
})
When you build advanced queries inline, pass inputs to describe the values that should trigger a resubscribe. This avoids tying updates to function identity and removes the need to wrap every advanced query in useMemo() or useCallback().
How list filters work
searchscans all string columns by defaultsearchInlimits search to specific string columnstimeframeusesupdated_atwhen available, otherwisecreated_atsortprefersupdated_at, then falls back tocreated_at
Generic variants
Generic variants are also available:
import { hooks } from '@hypertill/db/react'
const { data: notes } = hooks.useModels(Note, { search: 'hello' })
const { data: note } = hooks.useModel(Note, noteId)
Advanced reactive reads with withObservables
Use withObservables when you need custom reactive composition, counts, nested queries, or a graph of observables that goes beyond the default hooks.
Here is a practical TypeScript example that loads one book, its chapters, and a live count:
import { Q } from '@hypertill/db'
import { compose, withDatabase, withObservables } from '@hypertill/db/react'
type OuterProps = {
bookId: string
}
type InjectedProps = {
book: Book
chapters: Chapter[]
chapterCount: number
}
function BookDetail({ book, chapters, chapterCount }: InjectedProps) {
return (
<>
<h1>{book.title}</h1>
<p>{book.author}</p>
<p>{chapterCount} chapters</p>
<ul>
{chapters.map((chapter) => (
<li key={chapter.id}>{chapter.title}</li>
))}
</ul>
</>
)
}
export default compose(
withDatabase,
withObservables(['bookId'], ({ database, bookId }) => {
const books = database.get<Book>('books')
const chapters = database.get<Chapter>('chapters').query(
Q.where('book_id', bookId),
Q.sortBy('position', Q.asc),
)
return {
book: books.findAndObserve(bookId),
chapters,
chapterCount: chapters.observeCount(),
}
}),
)(BookDetail)
Why this split works
findAndObserve()is the cleanest way to keep one record live- passing the
chaptersquery directly is enough becausewithObservableswill observe it observeCount()gives you a cheap reactive count without loading another list into memory
Relations stay reactive too
If a model has a relation, you can observe it the same way:
const enhance = withObservables(['chapter'], ({ chapter }) => ({
chapter,
book: chapter.book,
}))
That keeps the component updated if the related record changes.
Use useDatabase for writes
useDatabase is the right tool for buttons, forms, screen actions, and command-style mutations:
import { useDatabase } from '@hypertill/db/react'
export function AddBookButton() {
const database = useDatabase()
const onPress = async () => {
await database.write(async () => {
await database.get<Book>('books').create((record) => {
record.title = 'Deep Work'
record.author = 'Cal Newport'
record.status = 'reading'
})
})
}
return <button onClick={() => void onPress()}>Add book</button>
}
This keeps writes explicit and easy to test.
Sorted lists and derived list updates
If your list ordering depends on mutable columns, use observeWithColumns() instead of plain observe():
const enhance = withObservables(['book'], ({ book }) => ({
chapters: book.chapters.observeWithColumns(['position']),
}))
You can also do this through advanced hooks:
const { data: chapters } = hooks.useChaptersAdvanced({
inputs: [bookId],
q: (Q) => [Q.where('book_id', bookId), Q.sortBy('position', Q.asc)],
observeWithColumns: ['position'],
})
That way the UI updates when the sort order changes, not just when rows are inserted or deleted.
When to use which
hooksfor the default live read pathwithObservablesfor complex composition, counts, or custom reactive graphsuseDatabasefor writes and imperative logic