State is your app’s memory. As applications evolve from simple prototypes to multi‑team products, managing that memory—what the user is doing, what data is loaded, what changes are pending—becomes one of the hardest engineering problems. The trick is understanding that not all state is the same and choosing the least complex tool that fits each category.
Redux and the Unidirectional Flow #
Redux popularized a contract: a single store, immutable updates, and pure reducers responding to descriptive actions. The payoff is predictability—time‑travel debugging and easy reasoning about “what caused what.” The cost is ceremony: action types, reducers, selectors, middleware. Modern ergonomics (Redux Toolkit) keep the contract while eliminating much of the boilerplate.
Ergonomics and Simplicity #
Not every app needs Redux. Framework‑native primitives often go far:
- React Context for scoped cross‑cutting state (theme, auth)
- Zustand/Jotai/Recoil for minimal, composable stores
- Vue’s Pinia and Svelte Stores for first‑class reactivity with tiny APIs
These options reduce friction and keep the cognitive load low.
Client State vs. Server State #
A pivotal realization: “client state” and “server state” have different lifecycles. UI state is ephemeral and local (is a modal open? current step?). Server state is remote truth (products, profile), and our UI holds a cache of it.
Trying to treat server data like client state leads to bespoke fetch code, ad‑hoc caching, and bugs around revalidation. Libraries like TanStack Query/SWR embrace the nature of server state: fetching, caching, background revalidation, automatic deduping, and mutation workflows that optimistically update and then reconcile with the server.
Practical Guidance #
Start with primitives. Use component state and props. Lift state only as needed.
Add a small client‑state library when coordination becomes complex across many siblings or pages.
Treat server data as cache. Adopt TanStack Query/SWR early to remove fetch logic from components, manage error/loading consistently, and keep data fresh.
Model “pending” and “optimistic” updates explicitly. Show progress, roll back on failure, and consolidate error handling.
Co-locate state. Keep related state near where it’s used. Avoid a single “god” store unless you need cross‑cutting global coordination.
Conclusion #
There is no one‑size‑fits‑all solution. Mature systems often mix approaches: a server‑state library for IO, a lightweight client store for shared UI state, and component state for local interactions. The key is intentionality—choose the simplest tool that fits the job today, and refactor as the product and team evolve.