Skip to content

Work / Aakalan / Wiki / Concepts

data-seam

Conceptverified 2026-10-30

data-seam

src/data/ is split so a change to how state is HELD cannot reach how it is COMPUTED.

File Job
reducer.ts The pure half: types, EMPTY, reducer. No React, no I/O. Every state test drives it directly.
actions.ts Every write, over dispatch + getState. Applies locally FIRST, queues the server write behind it.
store.ts The Zustand store, derived balances, and effects (auth, hydration, persistence, the live subscription).
hooks.ts The seam. What screens read.
  • Screens never import @/data/store or @/services/firebase. Money is derived in the hooks from expenses and settlements, never stored, so an action updates every screen at once and no two views can disagree.
  • State lives outside React, which is why Zustand: sync, RecurringRunner and ReceiptUploader all read and write the ledger and none is a screen. actions is bound once at module scope, so a background runner or deep link can fire one with no component in scope, and getState() always reads the current ledger.
  • Derived balances are computed once per dispatch, not per render. groupBalance and groupBalances are rebuilt with them on purpose: they are closures over the ledger and hooks.ts lists them in memo dependencies. A module-stable function would leave a group's rows stale the moment an expense was added.
  • src/lib/ is pure, with no I/O and no React, which is why it is the part under test.

Related: sync-gates, local-first-ledger