
Introduction
This article will explore the different ways to maintain “state”—the memory of your React application.
Choosing the right state management approach can be the difference between a clean, scalable app and a “prop-drilling” nightmare. This guide will compare the four most common approaches: useState, useReducer, Context API, and External Stores.
Managing State – Do’s and Do Not’s
When architecting your React app, follow these best practices to keep your data flow predictable.
- DO keep state as local as possible. If only one component needs it, use
useState. - DO use
useReducerwhen one state change affects multiple sub-values (like a form) or when the state value is a result of complex logic or calculations. - DON’T put everything in a global store; it makes debugging harder and can slow down performance.
- DO lift state up to the nearest common ancestor when two sibling components need the same data.
- DO return a new object in your Reducer rather than mutating the old one.
| Method | Best For… | Complexity | Scaling |
| useState | Local UI toggle, simple inputs | Low | Limited |
| useReducer | Complex logic, related state pieces | Medium | Medium |
| Context API | Theming, User Auth, Language | Medium | Medium |
| External | Complex data, high-frequency updates | High | Excellent |
Defining the Scenarios
Our comparison focuses on four specific use cases. This post highlights the implementation of the first three, with a deep dive into external stores coming in a later post.
- Local State: Managing a simple toggle or text input.
- Complex Local State: Managing a character’s stats (Health, Mana, XP) in a game.
- Shared State: Passing a “Dark Mode” setting across the entire app.
- Complex Global State: Managing a shopping cart with persistent storage.
Implementation Guide
1. Local State with useState
Most of your state should live here. It’s fast, simple, and built into React.
JavaScript
import React, { useState } from 'react';
function Counter() {
// Define a state variable called "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}2. The “Mission Control” with useReducer
When your state logic gets hairy—like a game character with multiple stats—useReducer is your best friend. It separates the “what happened” (Action) from the “how to update” (Reducer).
JavaScript
// The Reducer function handles all the logic outside the component, in one place
function reducer(state, action) {
switch (action.type) {
case 'attacked': return { ...state, hp: state.hp - 10 };
case 'heal': return { ...state, hp: state.hp + 10 };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, { hp: 100 });
// Trigger it anywhere
dispatch({ type: 'attacked' });
Pros: Centralized logic, easier to test, great for complex objects.
Cons: More boilerplate code than a simple useState.
3. Global Strategy with Context API
When you need to share data without passing props manually through every level, use Context.
JavaScript
const ThemeContext = React.createContext('light');
// ... wrap your app in <ThemeContext.Provider>
const theme = useContext(ThemeContext);
Pros: Included in React, avoids prop-drilling.
Cons: Can cause unnecessary re-renders if not managed carefully.
Which one should you use?
Open your code editor and look at your component tree.
- If the data is a single value: useState.
- If the data is an object with related properties: useReducer.
- If the data is “Global” (User info, Theme): Context API.
- If the data is massive and changes constantly: Zustand or Redux.
The result of picking the right tool might not look “exciting” in the code itself, but your future self—who has to debug this in six months—will definitely give you a pat on the back!
Alternative state management frameworks such as Redux will be addressed in a future post. Keep your eyes peeled!