Component-Level vs Global State
The architectural decision of whether state should live within a single component, be lifted to a shared ancestor, or be placed in a global store based on its scope and consumer count.
Description
One of the most impactful decisions in frontend architecture is determining where state should live. Component-level (local) state is owned by a single component and managed via useState, useReducer, or framework equivalents. It is the simplest and most performant option because updates only affect the owning component. Lifted state moves data to a common ancestor of the components that need it, passing it down via props. Global state (Redux, Zustand, Context) makes data accessible to any component in the tree without prop drilling.
The principle of state colocation dictates that state should live as close as possible to where it is used. Form input values, toggle states, local UI animations, and transient interaction state should almost always be local. Data shared between sibling components should be lifted to their nearest common parent. Only truly global concerns—authenticated user, theme preferences, feature flags, notification queues—belong in a global store. Server-cached data (API responses) should be managed by a dedicated server-state library (React Query, SWR, Apollo Client) rather than stuffed into a global store.
The most common mistake in frontend architecture is over-globalizing state. Putting everything in Redux or Context causes unnecessary re-renders across the component tree, creates coupling between unrelated features, and makes components impossible to reuse or test in isolation. A well-architected application typically has: local state for UI interactions, React Query or equivalent for server state, a small global store for auth and preferences, and URL state (search params, path) for navigation-related state. Each category has different caching, persistence, and synchronization requirements.
Prompt Snippet
Enforce a strict state placement hierarchy: 1) useState/useReducer for all component-scoped UI state (form inputs, toggles, modals), 2) React Query with queryClient for all server-fetched data with staleTime of 5 minutes for list queries and Infinity for reference data, 3) Zustand with selectors for global client state (auth session, feature flags, toast queue), 4) URL search params via nuqs or next/navigation for filterable/shareable state. Establish an ESLint rule or PR review checklist that flags any server response data being stored in Zustand or Context, and document the decision tree in an ADR.
Tags
Related Terms
Redux / Global State Patterns
A predictable state container pattern where all application state lives in a single store, updated exclusively through dispatched actions processed by pure reducer functions.
Stale-While-Revalidate
A caching strategy that immediately returns cached (potentially stale) data to the caller while asynchronously revalidating the cache in the background.
Optimistic Updates
A UI pattern where the interface immediately reflects a user's action as if it succeeded, then reconciles with the server response asynchronously—rolling back on failure.
Cache Invalidation Strategies
Techniques for determining when cached data is stale and must be refreshed, including time-based expiry, event-based invalidation, and tag-based dependency tracking.