watermelondb
SKILL.md
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@writermethods), the observable emits the same object reference with updated properties — it does not emit a new model instance.
React useState bailout
useStateusesObject.isto 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, updatephraseStatewith 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, sosetState(results)usually triggers re-renders.- If you build derived data (e.g.
linked.filter(...),assignments) in the subscribe callback andsetStatethat, 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 intosetState(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(), ... })). SeeAttemptCardinfeatures/lesson/components/AttemptCard.tsx. - For route params (e.g.
id) you typically subscribe manually inuseEffect(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; thencollection.findAndObserve(id)orquery.observe().subscribe(...). - Schema/tables:
database/schema.ts; collection access viadb.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).
Weekly Installs
28
Repository
jchaselubitz/drill-appFirst Seen
Jan 25, 2026
Security Audits
Installed on
codex25
gemini-cli25
opencode25
github-copilot23
kimi-cli18
amp18