[Guide] Build a Personal Finance Tracker - implementation notes
The Personal Finance Tracker guide is basically a love letter to stored derived state: totals are treated as primary data so list views are always ready and don't recompute on every interaction, and the "document per month" boundary keeps the ledger + drafts + derived totals scoped to a segment while global route state holds cross-screen filters.
Do you like 'document per month' as a boundary, or would you rather keep everything in one ledger with filters? If derived totals are primary data, how do you keep them from drifting away from raw transactions over time? The guide keeps DOM truth and state truth alive together and validates during render. How do you make that debuggable when something feels inconsistent?
Comments (12)
Back to latestDocument per month is a great mental model. It matches how people actually think about budgets and statements.
It also keeps derived totals scoped so you don't accidentally recalc the universe.
If derived totals are primary, then recalculation is a migration tool: you can replay and re-derive when you change rules.
But you need some evidence (derivedAt/version) so you can tell what model produced the totals you are looking at.
Related: Core Concepts Best Practices and Performance and Rendering Best Practices.
Global filters in route state is underrated. Finance screens are all the same arguments: currency, category, include transfers, sort, view.
If those keys are shared, the app feels coherent instead of each screen having its own preferences.
Counterpoint: once totals are stored, people stop questioning them. If a bug corrupts totals, it looks authoritative.
I'd want an explicit way to trigger a re-derive and an indicator when the totals were last recalculated.
Yeah, 100%. If totals are "primary", they need provenance like anything else.
We added a derived version + derivedAt and we render it in internal builds so screenshots carry the evidence.
Exactly. Without provenance, stored derived data turns into a lie that looks like truth.
Validating during render is brave but it fits the guide's posture of keeping behavior visible.
If validation is a contract, render the contract (what failed, where) instead of hiding it in handlers.
Related: Forms and Validation Best Practices.
The remount key note for insights is sneaky-good.
Charts are where you discover that "derived state" can be both the cure and the disease if you keep re-deriving during interaction.
If you treat totals as primary, make the derivation a versioned contract so you can migrate it.
ts
type Totals = { income: number; spend: number; net: number };
type TotalsMeta = { derivedAt: number; derivedVersion: number };
doc.write('totals', totals);
doc.write('totalsMeta', { derivedAt: Date.now(), derivedVersion: 3 } satisfies TotalsMeta);
Then when you change rules (category rollups, transfers), you can tell which months are stale without guessing.
We copied the "filters in global route state" bit and it made the app feel way more coherent.
The win wasn't just sharing state, it was being able to reproduce issues from a URL + screenshot because the route keys were stable.
Related: Routing and Navigation.
Tiny pattern for making render-time validation feel less mystical: render a compact validation summary as evidence.
tsx
const errors = validateDraft(doc.read('draft'));
doc.write('draftErrors', errors);
return <div data-draft-errors={String(errors.length)} />;
Even if you don't show it to users, it keeps the behavior legible for QA/support.
I like the "duplicate reads" posture in the guide (table and editor both reading from the document).
It feels wasteful until you realize it avoids stale editor UI where the list is fresh but the draft isn't.
Document-per-month also makes tests easier: snapshot one month document and you have a stable fixture for lots of UI states.
If everything is one global ledger, a small test ends up having to model the whole universe.
One question: how are people handling static export + enumerable month segments in practice?
The guide hints at determinism, but I'd be curious if teams pre-generate the last N months or treat it as a route contract that only certain months exist.