When React was first released, managing state was relatively simple—mostly handled within components using this.state
and setState
. As applications scaled, so did the complexity of managing state. This has led to a rich ecosystem of tools and approaches to solve state management challenges effectively.
Let’s walk through the evolution of React state management and when to use each approach.
1. useState
: The Starting Point
React Hooks introduced in version 16.8 changed everything.
const [count, setCount] = useState(0);
Use useState
when:
- You have local component state.
- The state logic is simple.
- No need to share the state across components.
2. useReducer
: For Complex Logic
For state with complex transitions (like toggling, incrementing, or managing arrays), useReducer
can be more predictable.
const reducer = (state, action) => {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
default: return state;
}
};
const [state, dispatch] = useReducer(reducer, { count: 0 });
Use useReducer
when:
- The state logic is complex or nested.
- You want to mimic Redux-style reducers.
3. Context API: For Global Sharing
React’s Context API helps pass state deeply without prop drilling.
const ThemeContext = React.createContext();
function App() {
return (
<ThemeContext.Provider value="dark">
<Header />
</ThemeContext.Provider>
);
}
Use Context when:
- You want to share simple global data (like theme or language).
- It’s read-heavy, not write-heavy.
Important: Don’t overuse Context for high-frequency updates—it can cause unnecessary re-renders.
4. Redux: The Enterprise Choice
Redux introduced a unidirectional data flow, perfect for large-scale apps.
Use Redux when:
- You need a single source of truth.
- You want middleware support (e.g., for logging or side effects).
- You’re building enterprise-level applications.
With Redux Toolkit, the setup is now easier than ever:
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
});
5. Recoil, Zustand, Jotai: The New Wave
Modern state libraries offer lightweight alternatives:
- Zustand: Minimal API, intuitive global state.
- Jotai: Atom-based primitives with first-class TypeScript support.
- Recoil: Atom + selector architecture for fine-grained updates.
Zustand example:
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
Use these when:
- You want a simpler and scalable global store.
- Redux feels like overkill for your project.
6. Server/URL-Based State: Don't Forget
Not all state belongs in React:
- Server State: Use tools like React Query or SWR for data fetching.
- URL State: Routing libraries like React Router manage state via URLs.
Conclusion: Choose the Right Tool
React's ecosystem provides a toolkit, not a one-size-fits-all. Here’s a quick decision tree:
- Local and simple? →
useState
- Complex transitions? →
useReducer
- Share globally? → Context / Zustand / Redux
- Async data? → React Query
- Heavy logic with debugging needs? → Redux Toolkit
As React apps grow, choosing the right state management strategy is critical to maintainability, performance, and scalability.
Top comments (0)