scrap / stop-storing-derived-state

Stop Storing Derived State

The life-OS site has a “balance quarter” page: a weekly point ledger with a running total. Each <WeeklyLog> block in the MDX took a previousBalance={N} prop and rendered the week’s net plus that opening number. I dropped a line into a Claude Code session: “Every week the balance from previous week is broken, making the balance quarter on itself not work.”

The chain had already drifted. Week 1’s data summed to a weekTotal of 12, but Week 2 declared previousBalance={48}. Every later week widened the gap. The point economy I was supposed to be tracking was dead, and I hadn’t noticed because the footer of each week still displayed a number.

Why it drifted

previousBalance is derived state — the sum of every prior week’s weekTotal - totalSpent. The MDX stored it as a hand-typed integer on every component. Touch one quest’s points and the contract breaks: the upstream weekTotal changes, the downstream previousBalance doesn’t. I’d have to remember to walk every downstream entry and adjust. I never did. Week by week, the numbers went fictional.

Delete the field.

The Astro snag

First instinct was a React context or a sibling component reading from somewhere. Doesn’t work. Each <WeeklyLog> was tagged client:load — its own hydration island. Astro renders sibling islands independently; they don’t share a React tree, so they can’t share state. The MDX-level pattern of one-component-per-week is structurally hostile to shared running totals.

Collapse from N islands to one. A wrapper <WeeklyTracker weeks={[...]}/> takes the whole series as a data array, computes the running balance in plain JS as it maps, and renders each <WeeklyLog> inside its single React tree with the computed previousBalance injected. One hydration island. Edit any past quest, the whole chain re-flows.

Accepting the recompute

Once I saw the drift the question was whether to preserve the displayed historical numbers (seed with a startingBalance so the chain “lands” on the existing totals) or recompute and let everything shift. I recomputed. The displayed Week 6 balance dropped from a drifted ~329 to a correct 147. The old numbers were wrong; they were always wrong; they just looked stable.

Two implementation details

Shared math helper. The wrapper computes weekTotal - totalSpent to thread the chain; the inner <WeeklyLog> computes the same thing for its footer. If those drift — someone changes the habit-points thresholds in one place but not the other — the chain and the display silently disagree. Both call into a tiny weeklyMath.ts. One source of truth for the economy table.

Narrative comments stay inline. Each week had a prose recap in a {/* Week N: ... */} MDX comment above its component — brag-doc context, what was happening that week, why points landed where they did. In the data-array shape they become // week N: ... JS comments above each object. Same authoring feel, invisible to readers, lives next to the data.

What shipped

  • src/components/content/weeklyMath.tscomputeWeekDelta() plus the habit-tier function. The point economy has one home.
  • src/components/content/WeeklyTracker.tsx — wrapper takes weeks, threads running balance, renders the inner <WeeklyLog> instances. Optional startingBalance prop if I ever want to seed from a prior quarter.
  • Two MDX files migrated. Balance-quarter (6 weeks) and paternity-quarter (13 weeks, archived but still rendering).
  • The balance-quarter skill updated to emit the new shape and to say it explicitly: never hand-edit a balance number.