State Management
What It Is
State management means organizing the data that changes over time and controls how the UI behaves.
In React, when state changes, React updates the UI so the screen reflects the latest data.
State Management = Where data lives + how it updates + how UI stays in sync
Why It Matters
For small components, local state is usually enough.
Examples:
- counter stores the current number
- login form stores email and password
- todo app stores task list
- dark mode switch stores the current theme
But real applications have many connected parts.
Examples:
- navbar
- sidebar
- cards
- profile section
- modals
- notifications
- cart
- dashboard
When multiple components need the same data, state management becomes important.
Small app -> useState is enough
Large app -> shared state needs structure
What State Management Solves
State management gives a clear structure for shared data.
It helps answer:
- where data should live
- how components should access data
- how updates should happen
- how UI should stay synchronized
- how to avoid duplicate state
- how to debug state changes
Without proper state management:
- components become tightly coupled
- prop passing becomes messy
- debugging becomes difficult
- updating shared data becomes frustrating
- UI can become inconsistent
Ecommerce Example
In an ecommerce app, cart data may be needed in many places.
Example:
Navbar -> cart count
Product Card -> add to cart
Cart Page -> cart items
Checkout Page -> same cart data
Wishlist -> depends on user state
Payment Page -> needs cart and user data
If each component stores its own copy of cart data, the app becomes inconsistent.
A better approach is to keep shared data in one clear place and let components read or update it through controlled patterns.
Local State
React provides local component state using useState().
Local state belongs only to the component where it is created.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function increase() {
setCount(count + 1);
}
return (
<div>
<h1>{count}</h1>
<button onClick={increase}>Increase</button>
</div>
);
}
export default Counter;
In this example:
countstores the current valuesetCountupdates the value- React re-renders the component when state changes
Local State = State that belongs to one component
Use local state when only one component needs the data.
When State Becomes Difficult
State becomes difficult when multiple components need the same data.
Common shared state examples:
- authentication state
- cart data
- theme settings
- notification count
- user profile
- language preference
If every component stores separate copies:
- inconsistencies appear
- bugs increase
- synchronization becomes difficult
That is why apps eventually need centralized or shared state management.
Product Card Example
A simple card component receives data through props and displays UI.
function ProductCard({ title, price, image }) {
return (
<div className="card">
<img src={image} alt={title} />
<h2>{title}</h2>
<p>₹{price}</p>
<button>Add to Cart</button>
</div>
);
}
Usage:
<ProductCard title="Wireless Headphones" price={2999} image="/headphone.png" />
This component is simple because:
- it receives data through props
- it displays UI
- it does not manage complicated logic
At this stage, advanced state management is not required.
Shared State in Product Cards
Now imagine the card needs to update the cart.
function ProductCard({ product, addToCart }) {
return (
<div className="card">
<img src={product.image} />
<h2>{product.title}</h2>
<p>{product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
);
}
Here:
- product data comes from parent
- button triggers shared cart update
- parent controls the main state
This introduces React's unidirectional data flow.
Unidirectional Data Flow
React follows unidirectional data flow.
This means data flows in one direction.
Parent Component -> Child Component
The parent owns the state and passes data down using props.
function Parent() {
const username = "Aman";
return <Child name={username} />;
}
function Child({ name }) {
return <h1>Hello {name}</h1>;
}
The child receives the data but does not directly modify parent state.
Updating Parent State from Child
A child can request an update by calling a function passed from the parent.
function Parent() {
const [count, setCount] = useState(0);
function increase() {
setCount(count + 1);
}
return <Child increase={increase} />;
}
function Child({ increase }) {
return <button onClick={increase}>Increase</button>;
}
Important points:
- state still belongs to the parent
- child only triggers the update
- parent controls how state changes
- data flow stays predictable
Data goes down as props.
Events go up as callbacks.
Why React Uses This Architecture
If every component could directly modify every other component, state changes would become unpredictable.
Unidirectional flow creates:
- structure
- predictability
- controlled updates
- centralized ownership of data
- easier debugging
- better maintainability
This is one reason React applications scale well.
Prop Drilling
Prop drilling means passing data through many intermediate components just to reach a deeply nested component.
Example structure:
App
└── Dashboard
└── Sidebar
└── Profile
Suppose Profile needs user data.
The data must pass through every component.
function App() {
const user = "Aman";
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Profile user={user} />;
}
function Profile({ user }) {
return <h1>{user}</h1>;
}
The problem:
Dashboarddoes not useuserSidebardoes not useuser- they only forward the prop
This unnecessary forwarding is called prop drilling.
Problems with Prop Drilling
In small apps, prop drilling is manageable.
In large apps, it becomes frustrating.
Common problems:
- deeply nested props are hard to track
- component APIs become bloated
- refactoring becomes painful
- debugging becomes harder
- unnecessary re-renders can happen
- components become tightly connected
- readability becomes worse
Example of messy props:
<Component
user={user}
theme={theme}
cart={cart}
language={language}
notifications={notifications}
/>
At this point, developers usually look for better shared state solutions.
State Management Solutions
Common solutions for shared state include:
- Context API
- Redux
- Zustand
- MobX
- Recoil
- Jotai
Each tool solves shared state problems differently.
The goal is not to use the most complex tool.
The goal is to choose the simplest tool that solves the problem properly.
Context API
Context API is built into React.
It allows components to access shared data directly without passing props through every intermediate component.
Instead of:
App -> Dashboard -> Sidebar -> Profile
You create:
Global Context -> Any Component
This avoids unnecessary prop drilling.
Creating Context
Create a context using createContext().
import { createContext } from "react";
export const UserContext = createContext();
This creates a shared container for the data.
Providing Context
Wrap child components with the provider.
import { UserContext } from "./UserContext";
function App() {
const user = "Aman";
return (
<UserContext.Provider value={user}>
<Dashboard />
</UserContext.Provider>
);
}
The provider shares the value with all child components inside it.
Consuming Context
Use useContext() to read context data.
import { useContext } from "react";
import { UserContext } from "./UserContext";
function Profile() {
const user = useContext(UserContext);
return <h1>{user}</h1>;
}
Now Profile can access user data directly.
No prop drilling is required.
Where Context API Works Well
Context API works well for shared data that does not need very complex update logic.
Good use cases:
- authentication
- themes
- language settings
- dark mode
- user preferences
It works especially well in medium-sized applications.
Limitations of Context API
Context API is powerful, but it is not always ideal for very large applications.
Possible problems:
- excessive re-renders
- difficult scaling
- complex update logic
- harder debugging in large systems
For large applications, dedicated state management libraries may be better.
Redux
Redux is a popular state management library in the React ecosystem.
Redux stores important application data in one predictable global store.
Redux = Centralized global store for frontend state
Instead of state being scattered across many components, Redux keeps shared data in one organized place.
Core Redux Concepts
Redux has four important concepts.
| Concept | Meaning |
|---|---|
| Store | Contains global application state |
| Action | Describes what happened |
| Reducer | Decides how state changes |
| Dispatch | Sends an action to Redux |
Store
The store contains the global application state.
Think of it as:
A central database for frontend state
Components can read from the store and trigger updates through actions.
Actions
Actions describe what happened.
They are plain JavaScript objects.
{
type: "ADD_TO_CART";
}
An action does not directly update the state.
It only describes the event.
Reducers
Reducers decide how state changes based on the action.
const initialState = {
count: 0,
};
function reducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return {
count: state.count + 1,
};
default:
return state;
}
}
Reducers should be predictable and pure.
Same state + same action -> same next state
Dispatch
Dispatch sends an action to Redux.
dispatch({
type: "INCREMENT",
});
Redux then passes the action to the reducer and updates the store.
Why Redux Became Popular
Redux solved many scaling problems in large apps.
Benefits:
- predictable updates
- centralized state
- organized architecture
- developer tools
- time travel debugging
- easier maintenance for large systems
Redux is useful when state updates need strong structure and predictability.
Redux Toolkit
Older Redux had a lot of boilerplate.
Modern Redux uses Redux Toolkit, which makes Redux cleaner and easier to use.
Redux Toolkit includes:
createSliceconfigureStore- React hooks
Redux Toolkit = Modern simpler way to write Redux
Redux Toolkit Example
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: {
count: 0,
},
reducers: {
increment(state) {
state.count += 1;
},
},
});
export const { increment } = counterSlice.actions;
export default counterSlice.reducer;
Redux Toolkit removes much of the unnecessary complexity from older Redux.
Other Modern Libraries
Redux is not the only solution.
Different projects may use different tools based on team size, project complexity, scalability needs, and developer preference.
| Library | Why Developers Like It |
|---|---|
| Zustand | Extremely simple and lightweight |
| Recoil | Flexible atom-based state |
| Jotai | Minimal and elegant |
| MobX | Reactive programming approach |
| Redux Toolkit | Best for large scalable apps |
Choosing the Right Approach
Do not use complex state management everywhere.
One beginner mistake is adding Redux to every project.
Good state management means choosing the simplest solution that solves the problem.
Small Projects
Small projects usually need:
useState- props
- local component state
- maybe Context API
Examples:
- portfolio websites
- simple CRUD apps
- landing pages
- small dashboards
Using Redux here can add unnecessary complexity.
Medium Applications
Medium applications may benefit from:
- Context API
- Zustand
- lightweight shared state
Examples:
- ecommerce frontend
- SaaS dashboard
- admin panels
Use these when multiple components need shared data, but the app does not require heavy architecture.
Large Applications
Large enterprise applications usually require:
- Redux Toolkit
- scalable architecture
- predictable state updates
Examples:
- social media platforms
- large dashboards
- collaborative systems
- enterprise tools
Large apps need predictable patterns because many features and teams may depend on the same state.
State Management Table
| Area | Explanation |
|---|---|
| Local State | State owned by one component |
| Props | Pass data from parent to child |
| Callbacks | Child requests parent update |
| Unidirectional Flow | Data down, events up |
| Prop Drilling | Passing props through unused intermediate components |
| Context API | Shared state without manual prop passing |
| Redux | Centralized predictable global state |
| Redux Toolkit | Modern simplified Redux |
| Zustand / Recoil / Jotai / MobX | Alternative state management libraries |
Basic Checklist
Start with local state
Keep state close to where it is used
Use props for parent-to-child data
Use callbacks for child-to-parent updates
Avoid duplicating the same state in many components
Watch for prop drilling in deep trees
Use Context API for shared app-level data
Use lightweight libraries for medium complexity
Use Redux Toolkit for large scalable apps
Do not add Redux just because the project uses React
Choose the simplest solution that solves the problem properly
Interview Style Answer
State management in React means organizing data that changes over time and controls how the UI behaves. For small components, local state using useState() is usually enough. As applications grow, multiple components may need shared data like authentication, cart items, theme, notifications, or user profile. React follows unidirectional data flow, where data moves from parent to child through props and updates are requested through callbacks. When props need to pass through many unused intermediate components, it creates prop drilling. Context API solves this by allowing components to access shared data directly. For larger applications, Redux provides a centralized global store with actions, reducers, and dispatch, while Redux Toolkit simplifies modern Redux. Other tools like Zustand, Recoil, Jotai, and MobX can also be used depending on project complexity.
One-Line Summary
State Management = Organize changing UI data so components can share, update, and stay synchronized predictably.
Final Mental Model
One component needs data -> useState
Parent to child -> props
Child updates parent -> callback
Too much prop passing -> Context API
Large predictable global state -> Redux Toolkit
Medium lightweight state -> Zustand or Context
Use the simplest solution that solves the problem.