WatermelonDB Model & Observation
Overview
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.
Observation & React Re-rendering
findAndObserve and same-reference emission
findAndObserve(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.
- When a model is updated (e.g. via
model.update() or @writer methods), the
observable emits the same object reference with updated properties — it
does not emit a new model instance.
React useState bailout
useState uses Object.is to decide whether to re-render. Passing the same
reference (e.g. setPhrase(model)) after an update means no re-render.
- Result: DB updates (text, note, language, etc.) don’t appear until the
user navigates away and back, when a new subscription yields a fresh
reference.
Fix: store a wrapper so each emit is a new reference
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;
- Use
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.
Query observe() and arrays
query.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.
- If you build derived data (e.g.
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.
- Use it when you can observe a model (or query) passed as a prop: e.g.
withObservables(['attempt'], ({ attempt }) => ({ attempt: attempt.observe(), ... })).
See AttemptCard in features/lesson/components/AttemptCard.tsx.
- For route params (e.g.
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.
Model patterns in this project
- Models:
database/models/ (e.g. Phrase, Lesson, Attempt,
Translation, Deck). Use @field, @writer, and static helpers (e.g.
Phrase.findOrCreatePhrase, Lesson.addLesson).
- Observation:
useDatabase() from @nozbe/watermelondb/react; then
collection.findAndObserve(id) or query.observe().subscribe(...).
- Schema/tables:
database/schema.ts; collection access via
db.collections.get<Model>(TABLE).
Quick reference
| 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) |
Resources
- DeepWiki: Nozbe/WatermelonDB —
findAndObserve, observe(), withObservables, model updates.
- Project:
PhraseDetailScreen, LessonDetailScreen, SetDetailScreen
(findAndObserve + wrapper); AttemptCard (withObservables).
1---2name: watermelondb3description: 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.4---5
6# WatermelonDB Model & Observation
7
8## Overview
9
10This skill covers WatermelonDB models (`database/models/`), observation
11(reactive queries), and **ensuring React re-renders when observed data
12changes**. Use it when working with `findAndObserve`, `query.observe()`,
13`withObservables`, or any screen that subscribes to DB changes.
14
15---
16
17## Observation & React Re-rendering
18
19### `findAndObserve` and same-reference emission
20
21- **`findAndObserve(id)`** (on a collection): Fetches a record by ID, returns an
22 Observable that emits **immediately** on subscribe and **whenever the record
23 is updated or deleted**.
24- When a model is updated (e.g. via `model.update()` or `@writer` methods), the
25 observable **emits the same object reference** with updated properties — it
26 does **not** emit a new model instance.
27
28### React `useState` bailout
29
30- `useState` uses `Object.is` to decide whether to re-render. Passing the **same
31 reference** (e.g. `setPhrase(model)`) after an update means **no re-render**.
32- Result: DB updates (text, note, language, etc.) **don’t appear** until the
33 user navigates away and back, when a new subscription yields a fresh
34 reference.
35
36### Fix: store a wrapper so each emit is a new reference
37
38When subscribing to a **single model** (e.g. `findAndObserve`) and storing it in
39React state, **don’t** store the raw model. Store a wrapper so every emission
40updates state with a **new object**:
41
42```ts
43const [phraseState, setPhraseState] = useState<
44 { phrase: Phrase; _key: number } | null
45>(null);
46
47useEffect(() => {
48 if (!id) return;
49 const sub = db.collections
50 .get<Phrase>(PHRASE_TABLE)
51 .findAndObserve(id)
52 .subscribe((result) => {
53 setPhraseState({ phrase: result, _key: result.updatedAt });
54 });
55 return () => sub.unsubscribe();
56}, [id, db]);
57
58const phrase = phraseState?.phrase ?? null;
59```
60
61- Use `phrase` (derived) everywhere in the component. Updates persisted to the
62 DB will re-emit, update `phraseState` with a new wrapper, and trigger a
63 re-render.
64
65### Query `observe()` and arrays
66
67- **`query.observe()`** emits **arrays** of models. When the query result set
68 changes, WatermelonDB typically emits a **new array** reference, so
69 `setState(results)` usually triggers re-renders.
70- If you **build derived data** (e.g. `linked.filter(...)`, `assignments`) in
71 the subscribe callback and `setState` that, you’re already passing new
72 references — no extra wrapper needed.
73
74---
75
76## `withObservables` (HOC)
77
78- **`withObservables(triggerProps, getObservables)`** injects observable values
79 as props and **always** passes a **new state object** into `setState` (e.g.
80 `{ values, isFetching }`), so React re-renders on each emission even when
81 model references are unchanged.
82- Use it when you can **observe a model (or query) passed as a prop**: e.g.
83 `withObservables(['attempt'], ({ attempt }) => ({ attempt: attempt.observe(), ... }))`.
84 See `AttemptCard` in `features/lesson/components/AttemptCard.tsx`.
85- For **route params** (e.g. `id`) you typically **subscribe manually** in
86 `useEffect` (e.g. `findAndObserve(id)`). In that case, use the **wrapper
87 pattern** above instead of storing the raw model.
88
89---
90
91## Model patterns in this project
92
93- **Models**: `database/models/` (e.g. `Phrase`, `Lesson`, `Attempt`,
94 `Translation`, `Deck`). Use `@field`, `@writer`, and static helpers (e.g.
95 `Phrase.findOrCreatePhrase`, `Lesson.addLesson`).
96- **Observation**: `useDatabase()` from `@nozbe/watermelondb/react`; then
97 `collection.findAndObserve(id)` or `query.observe().subscribe(...)`.
98- **Schema/tables**: `database/schema.ts`; collection access via
99 `db.collections.get<Model>(TABLE)`.
100
101---
102
103## Quick reference
104
105| Scenario | Pattern | Re-render guarantee |
106| ----------------------------------------- | ------------------------------------------------------------------------- | ------------------------------ |
107| Single model by `id` (e.g. detail screen) | `findAndObserve` + **wrapper state** `{ model, _key }` | Yes |
108| Query results (list) | `query.observe()` + `setState(results)` or derived structures | Yes (new array/refs) |
109| Model passed as prop | `withObservables(['model'], ({ model }) => ({ model: model.observe() }))` | Yes (HOC uses new state shape) |
110
111---
112
113## Resources
114
115- **DeepWiki**: [Nozbe/WatermelonDB](https://github.com/Nozbe/WatermelonDB) —
116 `findAndObserve`, `observe()`, `withObservables`, model updates.
117- **Project**: `PhraseDetailScreen`, `LessonDetailScreen`, `SetDetailScreen`
118 (findAndObserve + wrapper); `AttemptCard` (withObservables).