rgoussu@goussu: ~/library/frontend
~/library/frontend cat component-architecture-and-state.md

Component architecture & state management

# How UI code stays sane as it grows — one-way data flow, composition patterns, and the discipline of putting each piece of state where it belongs.

Conceptsaved 2026-08-08updated 2026-08-09 #frontend#react#state-management#components#architecture

Overview

Every non-trivial UI is a state machine wearing a costume, and most frontend pain is state put in the wrong place. Component architecture is the set of habits that keep the costume from tearing: one-way data flow, components composed rather than configured, and state classified by what owns it — local, shared client, server cache, or URL — before choosing any library. The framework matters less than the discipline; React is the lingua franca but the questions are identical in Vue, Svelte, or Angular.

Key points

  • One-way data flow is the constitution: data down via props, events up via callbacks; the moment two components write the same value from different directions, bugs become non-local.
  • Classify state before storing it: server state (someone else's data, needs caching/refetching/invalidation — TanStack Query, SWR, RTK Query), client state (yours truly — useState/useReducer, a store), URL state (filters, pagination, anything shareable — the router), form state (its own beast — react-hook-form). Most "we need Redux" moments dissolve once server state moves to a query cache.
  • Lift state to the lowest common owner, no higher: global stores are for genuinely global things (session, theme, feature flags); everything else colocated. Context is a dependency-injection mechanism, not a state manager — every consumer re-renders on change.
  • Composition over configuration: children and slots beat prop explosions; compound components (<Tabs>, <Tabs.Panel>) beat 30-prop monoliths; hooks extract behavior, headless components extract behavior + accessibility (Radix, React Aria).
  • Derive, don't duplicate: state that can be computed from other state must be computed, not stored — the second copy always drifts. Reach for useMemo/selectors, not another useEffect syncing copies (most useEffects are a design smell).
  • The store landscape, read coldly: Redux Toolkit (predictable, verbose, great devtools), Zustand (a store without ceremony), signals (Solid, Preact, Vue — fine-grained reactivity moving the recompute burden from vdom-diff to dependency graph). Choose per shape of writes, not fashion.

Details

Where each kind of state lives

Kind Smell when misplaced Home
Server cache "loading spinner flashes stale data", hand-rolled isLoading flags Query cache (TanStack Query / RTK Query)
Global client Prop-drilled session/theme through 6 layers Small store (Zustand/RTK) or context if write-rare
Local A component's open/closed flag in Redux useState next to its use
URL Filters lost on refresh or unshareable links Router / search params
Form Field values mirrored into a store keystroke by keystroke Form library, submitted as an event

Rendering discipline

Re-renders are the tax on state design: a render is a pure function of props + state, so the fix for a slow tree is structural (move state down, split components, memoize the expensive leaf) before it is memo-everything. Profiling first, per web performance.

Practice

  • TodoMVC twice (source) — once in vanilla TypeScript (manual DOM, hand-rolled store), once in React; what the framework actually buys you becomes visible, and so does what it costs.
  • Query-cache refactor (source) — take any fetch-heavy side project and move every server interaction into TanStack Query; count the useState/useEffect pairs that disappear.
  • Compound component kata (source) — build <Tabs> as a compound component with keyboard support, then compare with the Radix primitive; drills composition and the a11y you'd otherwise hand-roll.
  • RealWorld full-stack build (source) — the capstone: every state category present in one honest app.

Related