WatermelonDB models, observation patterns, and React integration. Use when writing or debugging model code, observers (findAndObserve, query.observe), or screens that display live-updating DB data.
This skill covers WatermelonDB models (database/models/), observation
(reactive queries), and ensuring React re-renders when observed data
changes. Use it when working with findAndObserve, query.observe(),
withObservables, or any screen that subscribes to DB changes.
findAndObserve and same-reference emissionfindAndObserve(id) (on a collection): Fetches a record by ID, returns an
Observable that emits immediately on subscribe and whenever the record
is updated or deleted.model.update() or @writer methods), the
observable emits the same object reference with updated properties β it
does not emit a new model instance.useState bailoutuseState uses Object.is to decide whether to re-render. Passing the same
reference (e.g. setPhrase(model)) after an update means no re-render.When subscribing to a single model (e.g. findAndObserve) and storing it in
React state, donβt store the raw model. Store a wrapper so every emission
updates state with a new object:
const [phraseState, setPhraseState] = useState<
{ phrase: Phrase; _key: number } | null
>(null);
useEffect(() => {
if (!id) return;
const sub = db.collections
.get<Phrase>(PHRASE_TABLE)
.findAndObserve(id)
.subscribe((result) => {
setPhraseState({ phrase: result, _key: result.updatedAt });
});
return () => sub.unsubscribe();
}, [id, db]);
const phrase = phraseState?.phrase ?? null;
phrase (derived) everywhere in the component. Updates persisted to the
DB will re-emit, update phraseState with a new wrapper, and trigger a
re-render.observe() and arraysquery.observe() emits arrays of models. When the query result set
changes, WatermelonDB typically emits a new array reference, so
setState(results) usually triggers re-renders.linked.filter(...), assignments) in
the subscribe callback and setState that, youβre already passing new
references β no extra wrapper needed.withObservables (HOC)withObservables(triggerProps, getObservables) injects observable values
as props and always passes a new state object into setState (e.g.
{ values, isFetching }), so React re-renders on each emission even when
model references are unchanged.withObservables(['attempt'], ({ attempt }) => ({ attempt: attempt.observe(), ... })).
See AttemptCard in features/lesson/components/AttemptCard.tsx.id) you typically subscribe manually in
useEffect (e.g. findAndObserve(id)). In that case, use the wrapper
pattern above instead of storing the raw model.database/models/ (e.g. Phrase, Lesson, Attempt,
Translation, Deck). Use @field, @writer, and static helpers (e.g.
Phrase.findOrCreatePhrase, Lesson.addLesson).useDatabase() from @nozbe/watermelondb/react; then
collection.findAndObserve(id) or query.observe().subscribe(...).database/schema.ts; collection access via
db.collections.get<Model>(TABLE).| Scenario | Pattern | Re-render guarantee |
|---|---|---|
Single model by id (e.g. detail screen) |
findAndObserve + wrapper state { model, _key } |
Yes |
| Query results (list) | query.observe() + setState(results) or derived structures |
Yes (new array/refs) |
| Model passed as prop | withObservables(['model'], ({ model }) => ({ model: model.observe() })) |
Yes (HOC uses new state shape) |
findAndObserve, observe(), withObservables, model updates.PhraseDetailScreen, LessonDetailScreen, SetDetailScreen
(findAndObserve + wrapper); AttemptCard (withObservables).