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