I once spent four hours debugging why a shopping cart was showing the wrong item count on the checkout page. The count in the header said 3 items. The checkout page said 2. Both were correct β they were reading from different copies of the same state that had gotten out of sync. I had state living in two places and no single source of truth.
Every intermediate frontend developer hits this wall. You're adding features, components multiply, and suddenly you realize you've been passing the same piece of data through six levels of props, or storing it in three different places that can disagree with each other. That's the state management problem. It's not about which library you use β it's about understanding why the problem exists and how the different solutions address it.
Every interactive web application has state β data that can change over time and that the UI needs to reflect. A shopping cart's item count, whether a modal is open, which user is logged in, the results of a search query β all of this is state. As applications grow, managing state correctly becomes one of the most complex challenges in frontend development.
What Is State, Exactly?
State is any data that your application needs to remember between events. It can be local β a specific component's concern, like whether a dropdown is expanded β or global, like the current user's authentication status that multiple components across the app need. The key insight is that state determines what the UI looks like at any moment. When state changes, the UI should automatically update to reflect it. React, Angular, and Vue all implement this principle: UI is a function of state.
The complexity arises when you have many components at different levels of the component tree that all need access to the same state, or when state changes in one place trigger updates in many others.
Local State: Keep It Simple
Not all state needs to be global. A form's input values, whether a tooltip is visible, or the active tab in a tabbed interface β these are local state concerns. React's useState hook, Angular's component properties, and Vue's reactive data properties handle this perfectly. Local state is simpler to reason about, easier to test, and doesn't pollute global state with concerns that don't belong there.
A critical skill is recognizing when state should be local versus shared. Before reaching for a global state manager, ask: does this state need to be accessed by components outside the current component tree? If no, keep it local.
Lifting State: Sharing Between Siblings
When two sibling components need to share state, the standard solution is to "lift" the state up to their nearest common ancestor. The parent holds the state and passes it down as props. This works well for shallow component trees but creates "prop drilling" when state needs to pass through many intermediate levels of components that don't use it themselves.
Prop drilling is a symptom, not a crisis β it indicates that either your component tree is too deep, your state is too global, or you need a different approach to sharing state.
Context API: Built-In Global State
React's Context API (and its equivalents in Angular's Dependency Injection and Vue's provide/inject) allows any component in the tree to access a value without prop drilling. Context is excellent for truly global concerns: the current user, the active theme, the user's language preference, or application-wide configuration.
Context has important limitations. Every component that consumes a context will re-render when that context value changes β even if the specific part of the value the component uses didn't change. This makes Context unsuitable for frequently-changing state in large applications without careful memoization.
Redux: Predictable, Centralized State
Redux became the dominant state management solution in the React ecosystem because it solved the predictability problem. In Redux, all application state lives in a single store β a plain JavaScript object. State can only change through actions β plain objects describing what happened. Reducers β pure functions β determine how state changes in response to actions. This strict, one-way data flow makes state changes predictable, traceable, and testable.
The Redux DevTools extension lets you inspect every state change, time-travel through the application's history, and even replay actions. This debugging superpower made Redux popular for complex applications where understanding exactly what happened and when is critical.
Redux Toolkit (RTK) modernized Redux by eliminating much of the boilerplate: createSlice generates action creators and reducers together; RTK Query handles data fetching and caching; createAsyncThunk manages async operations cleanly.
NgRx: Redux for Angular
Angular's ecosystem answered with NgRx β an Angular-specific implementation of the Redux pattern using RxJS Observables. Actions, reducers, effects (for side effects like API calls), and selectors work together in a strictly structured way. NgRx adds significant boilerplate but provides powerful patterns for large enterprise Angular applications where predictable state management across many teams is critical.
Zustand and Jotai: Modern Lightweight Alternatives
The React ecosystem has moved toward simpler, less opinionated state management libraries. Zustand uses a simple store with no actions or reducers β just functions that modify state directly. Jotai takes an atomic approach β individual pieces of state (atoms) are created independently and composed together, with fine-grained reactivity so components only re-render when their specific atoms change.
These libraries have far less boilerplate than Redux while still providing shared global state. For most new projects without massive team size or extreme complexity, Zustand or Jotai are excellent choices.
Choosing the Right Approach
For small applications, local state and Context are sufficient. For medium applications with significant shared state, Zustand offers a good balance of simplicity and capability. For large enterprise applications with many developers, complex workflows, and strong debugging requirements, Redux Toolkit or NgRx provide the structure and tooling to manage that complexity reliably. The best state management solution is the simplest one that handles your actual complexity β not the one that handles all possible future complexity.
