Frontend Architecture
State shape: refactor the state soup
Reading about shape mistakes is not the same as untangling them in a real app. Build (or take) a filterable list-plus-detail dashboard whose state is deliberately a mess — fetched data in useState, a stored count, filters lost on refresh, a global store re-rendering everything — and refactor each value into the right place, proving the win at every step.
Turn the unit’s decision tree into a working refactor: classify every piece of state, move server data to a cache library, shareable view-state to the URL, private state down to its lowest reader, delete what is derivable, and normalize the relational data you mutate — then prove fewer bugs and fewer re-renders.
Take a 'state-soup' filterable dashboard (your own or the starter described below) where every value is in the wrong place, and refactor it so each piece of state lives in its correct shape — server cache, URL, colocated client state, or derived — with measurable proof that drift bugs and re-renders went down.
- A before/after state inventory table showing each value's old home, new home, and the rule that moved it; nothing fetched lives in useState, nothing derivable is stored, and shareable view-state lives in the URL.
- Refresh and copy-link both reproduce the exact filtered/sorted/paged view with the same detail open — demonstrated, not assumed.
- A re-render count (React DevTools Profiler or a render-logging hook) before and after, showing typing in the filter or toggling a menu no longer re-renders unrelated subtrees such as the detail panel or charts.
- A short write-up explaining, for each moved value, why its new home is correct and which shape bug (drift, lost view-state, re-render storm, stale snapshot) the move prevents.
- Add an optimistic mutation (rename/edit a record) through the cache library and show the list, badge, and open detail all stay consistent with no manual count or copy to update.
- Replace ad-hoc query-string parsing with a typed URL-state layer (a small useSearchParams wrapper or a router's typed search params) so filters round-trip safely and invalid values fall back to defaults.
- Add a 'shape lint' check to CI that fails if a fetched payload is stored in useState or if a known-derivable value (count, total) appears in state, to stop the soup creeping back.
- Measure and report the network win: confirm the two reader components now issue one shared request instead of two, and that a filter change refetches once with dedupe rather than per-component.
This is the refactor you will run on real codebases: inventory every value, then route each to its correct home — fetched data to a server cache keyed by request, shareable view-state to the URL, private state down to its lowest reader, derivable values deleted, and mutated relational data normalized to a byId map. The proof is concrete: refresh and copied links reproduce the view, re-renders shrink to the owning subtree, and the drift bugs become impossible because there is no second source of truth left to drift. Doing it once on a toy dashboard makes the production untangle muscle memory.