Composition over oversized components
Split screens into primitives, feature components, and route containers so state and accessibility responsibilities stay discoverable.
We treat React as a UI library and design explicit ownership for local state, shared UI state, server data, URL state, and accessible interaction—without assuming one global store.
Component ownership and data-flow map
An observatory of where UI state, server data, and accessible output belong. Distinct from a deployment pipeline.
Layer 1
Component composition
Presentational primitives and feature components arranged into screens.
Layer 2
Local UI state
Ephemeral interaction state owned by the nearest interactive component.
Layer 3
Shared UI state
Lifted or contextual state only when multiple siblings truly collaborate.
Layer 4
Server data
Remote records fetched and normalized outside pure UI rendering.
Layer 5
Derived values
Computed from existing state/props—usually not stored again.
Layer 6
Effects / integrations
Synchronization with APIs, analytics, or browser APIs after render intent is clear.
Layer 7
Accessible output
Roles, names, focus, and status text that make the interaction operable.
Illustrative interface systems—not a guaranteed delivery catalog.
Composable panels, filters, and configuration UIs with clear local vs shared state boundaries.
Multi-step workflows with controlled inputs, error/empty states, and keyboard-complete paths.
URL-synchronized filters, derived result sets, and server-state hydration without duplicating truth in React state.
Pick a product situation to see a typed ownership model. Context is not a default; third-party stores are not automatic.
Pick a product situation to see a typed ownership model. Context is not a default; third-party stores are not automatic.
Situation
Multi-field input with validation, submit pending, and inline errors.
local
Field values & touched flags
Keep field state near the form unless other distant regions must edit the same draft.
May affect
Form root · Field controls · Inline error text
derived
Validation messages
Derive validity from values and schema; avoid mirroring errors in a second writeable store.
May affect
Field controls · Submit button disabled state
server
Submit result
Persist through an API/Server Action; reflect pending/error/success as explicit UI states.
May affect
Submit control · Status region
url
Deep-linked step (optional)
If the wizard must be shareable/restorable, encode step in the URL instead of only memory.
May affect
Wizard shell · Step navigation
Do not push every keystroke into global context. Do not store derived validation strings as authoritative state.
Practical composition patterns for maintainable React trees—not a claim that one pattern fits every product.
Split screens into primitives, feature components, and route containers so state and accessibility responsibilities stay discoverable.
Choose controlled inputs when the parent must orchestrate validation; uncontrolled can be fine for simple isolated fields.
Primitives stay product-agnostic. Feature components may know domain props but should not become global dumping grounds.
Use compound patterns for cohesive widgets (tabs, steppers) that share private context without exposing it app-wide.
Extract reusable behavior—not junk drawers. Hooks should not hide server fetching behind opaque side effects without explicit states.
Keep remote records distinct from ephemeral UI flags to prevent sync bugs and duplicated sources of truth.
Every data-backed region needs explicit non-happy paths with actionable copy—not spinners alone.
Custom widgets inherit the duty for names, focus order, keyboard operation, and status announcements.
Derived values instead of mirrored state
Illustrative pattern—derive totals from selections rather than storing a second writeable total.
type OptionId = "basic" | "pro" | "scale";
type Selection = {
optionId: OptionId;
seats: number;
};
const PRICE: Record<OptionId, number> = {
basic: 20,
pro: 45,
scale: 90,
};
export function priceFor(selection: Selection): number {
return PRICE[selection.optionId] * selection.seats;
}Narrow provider instead of app-wide context
Illustrative pattern—limit shared UI state to the feature subtree that needs it.
type WizardValue = {
step: number;
answers: Record<string, string>;
setStep: (step: number) => void;
};
const WizardContext = createContext<WizardValue | null>(null);
export function useWizard(): WizardValue {
const value = useContext(WizardContext);
if (!value) {
throw new Error("useWizard requires WizardProvider");
}
return value;
}How updates can move through a tree. Re-renders are normal; optimize from evidence.
01
The component that calls the setter owns the write path.
02
Children re-render when received props/context values change.
03
Pure calculations update when inputs change—no extra store required.
04
Memo/PureComponent boundaries are tools for measured hot paths—not defaults.
05
Query/subscription libraries notify UI when remote data identity changes.
A re-render is not automatically a bug. Optimize from evidence—profiling and user-perceived latency—not from fear of renders.
A React-specific engineering sequence centered on ownership, accessibility, and release hardening.
List user intents, ephemeral UI flags, shareable parameters, and remote records before choosing libraries.
Draw primitive vs feature vs route containers so ownership and testing seams are obvious.
Define how server data enters the tree, how mutations complete, and how failures surface.
Specify tab order, focus returns, names for icon controls, and non-color status cues up front.
Design non-happy paths as first-class UI, including retry and partial-data presentations.
Prefer user-centric component tests for interactions; add integration coverage for critical state machines.
Measure before memoizing; budget client islands; verify no meaningful content is client-only.
Practical problems to design against when composing interfaces and state.
God components mix fetching, layout, and business rules until changes become unsafe.
The same fact stored in props, context, and local state drifts out of sync.
Effects used to compute derivable values create timing bugs and extra renders.
Handlers capture old state when dependencies are incomplete or setters are misused.
Index keys on reorderable lists scramble state and accessibility relationships.
Totals and labels mirrored into writeable state become inconsistent with sources.
App-wide providers for frequent updates amplify render fan-out.
Divs with onClick skip keyboard users and assistive technologies.
useMemo/useCallback everywhere obscures intent without proven benefit.
Putting critical explanations only inside client trees harms SEO and no-JS users.
Silent blank panels appear when networks fail or datasets are empty.
State ownership recommendations are educational decision models. They are not universal performance guarantees or library mandates.
Inventory interaction and state before coding. Prefer composition and derived values. Profile before memoizing. Verify keyboard and screen-reader paths for custom controls.
No. React is a UI library. Routing, data fetching, SEO metadata, and deployment usually come from a framework or surrounding architecture such as Next.js.
No. Prefer local state, lifted state, URL state, or server state first. Context and shared stores help when many distant components genuinely share UI concerns—not as a default.
No. Memoization can help when profiling shows expensive re-renders, but premature memoization adds complexity without guaranteed benefit.
Design React systems as an observatory of ownership: know what is local, lifted, derived, or remote—and keep meaningful content outside the client-only zone.
I would like to discuss React component composition, state ownership, and accessible interface design for our product.
Begin stack consultation