State Management in React.js
State management in React.js is about how you store, update, and share data (state) across components. As your app grows, choosing the right approach becomes very important.
I will explain this from basics to advanced, with when to use what, keeping real-world projects in mind.
What is State in React?
State is data that changes over time and affects what is rendered on the UI.
Examples:
- Form input values
- Logged-in user data
- Cart items
- API response data
- UI state (modal open/close)
Local State (useState)
Used when state belongs to a single component
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</>
);
}
Best for
- Simple UI state
- Small components
Not good for
- Sharing data across many components
Derived State (Props)
Parent => Child data flow
function Parent() {
const [user, setUser] = useState("Jitendra");
return <Child user={user} />;
}
function Child({ user }) {
return <h1>Hello {user}</h1>;
}
React's recommended first approach
Lift state up
Global State with Context API
Used when many components need the same state
Example: Auth Context
import { createContext, useContext, useState } from "react";
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user, setUser }}>>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
Usage:
const { user } = useAuth();
Best for
- Auth
- Theme
- Language
- User settings
Avoid for
Very frequent updates (performance issues)
useReducer (Better for Complex State)
When state logic is complex (like Redux style)
function reducer(state, action) {
switch (action.type) {
case "ADD":
return { count: state.count + 1 };
case "SUB":
return { count: state.count - 1 };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
Best for
- Complex forms
- State transitions
- Predictable updates
Redux / Redux Toolkit (Enterprise Standard)
For large applications
Why Redux Toolkit?
- Less boilerplate
- Built-in best practices
- DevTools support
Example:
import { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: [],
reducers: {
addItem: (state, action) => {
state.push(action.payload);
},
},
});
export const { addItem } = cartSlice.actions;
export default cartSlice.reducer;
Best for
- E-commerce
- Dashboard apps
- Multi-user state
- Complex workflows
Zustand (Modern & Lightweight)
Very popular alternative to Redux
import { create } from "zustand";
const useStore = create((set) => ({
cart: [],
addToCart: (item) =>
set((state) => ({ cart: [...state.cart, item] })),
}));
Best for
- Medium to large apps
- Simple API
- High performance
Server State (React Query / TanStack Query)
For API data, NOT UI state
useQuery({
queryKey: ["products"],
queryFn: fetchProducts,
});
Handles:
- Caching
- Background refetch
- Pagination
- Loading & error states
Best for
- REST / GraphQL APIs
- Real-time data
- Large data fetching
State Management Comparison
| Use Case | Best Choice |
|---|---|
| Simple UI state | useState |
| Parent-child sharing | Props |
| App-wide config | Context API |
| Complex logic | useReducer |
| Large enterprise app | Redux Toolkit |
| Lightweight global state | Zustand |
| API data | React Query |
Best Practices (Important)
- Keep state as local as possible
- Separate UI state and Server state
- Don't overuse Context
- Prefer Redux Toolkit or Zustand over old Redux
- Use React Query for APIs