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 @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/WatermelonDBfindAndObserve, observe(), withObservables, model updates.
  • Project: PhraseDetailScreen, LessonDetailScreen, SetDetailScreen (findAndObserve + wrapper); AttemptCard (withObservables).
Weekly Installs
28
First Seen
Jan 25, 2026
Installed on
codex25
gemini-cli25
opencode25
github-copilot23
kimi-cli18
amp18