Skip to main content

State Management

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

One-line idea: State Management = Organize changing UI data so components can share, update, and stay synchronized predictably.

Core Concepts​

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

Practical Examples​

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:

  • count stores the current value
  • setCount updates 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:

  • Dashboard does not use user
  • Sidebar does not use user
  • 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.

ConceptMeaning
StoreContains global application state
ActionDescribes what happened
ReducerDecides how state changes
DispatchSends 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.

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:

  • createSlice
  • configureStore
  • 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.

LibraryWhy Developers Like It
ZustandExtremely simple and lightweight
RecoilFlexible atom-based state
JotaiMinimal and elegant
MobXReactive programming approach
Redux ToolkitBest 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.

Choosing the Right Approach: 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.

Choosing the Right Approach: 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.

Choosing the Right Approach: 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​

AreaExplanation
Local StateState owned by one component
PropsPass data from parent to child
CallbacksChild requests parent update
Unidirectional FlowData down, events up
Prop DrillingPassing props through unused intermediate components
Context APIShared state without manual prop passing
ReduxCentralized predictable global state
Redux ToolkitModern simplified Redux
Zustand / Recoil / Jotai / MobXAlternative state management libraries

Summary​

State Management = Organize changing UI data so components can share, update, and stay synchronized predictably.

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.