State Management
Manage application state effectively with various strategies
What you'll learn
Understand when local state is sufficient vs when global state is needed
Compare Context, Redux, Zustand, and Jotai for different scale needs
Implement a Zustand store for simple global state
Structure Redux with slices, selectors, and middleware for complex state
Recognize common anti-patterns: over-centralization, mutable updates, selector inefficiency
Use URL state and server state as first-class state sources
State management is about where and how your data lives as the user interacts with your app.
Types of State
Local State
State that belongs to a single component:
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
Lifted State
Shared between sibling components via their closest common ancestor:
function Parent() {
const [value, setValue] = useState("")
return (
<>
<Input value={value} onChange={setValue} />
<Preview text={value} />
</>
)
}
Global State
Accessible anywhere in the app — context, Redux, Zustand, etc.
useState — Local State
The simplest form of state. Use it when state only affects one component.
function Toggle() {
const [isOn, setIsOn] = useState(false)
return <button onClick={() => setIsOn(!isOn)}>{isOn ? "ON" : "OFF"}</button>
}
useReducer — Complex State
Use when state updates follow multiple paths:
type Action = { type: "increment" } | { type: "decrement" } | { type: "reset" }
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment":
return state + 1
case "decrement":
return state - 1
case "reset":
return 0
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0)
return (
<>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</>
)
}
Context API — Shared State
Use React Context to avoid prop drilling:
const ThemeContext = createContext("light")
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
)
}
function Toolbar() {
const theme = useContext(ThemeContext)
return <div className={theme}>Themed content</div>
}
When to Use What
| Pattern | When |
|---|---|
useState | Single component, independent values |
useReducer | Complex state logic, multiple update paths |
| Context | Shared state across a subtree, few consumers |
| Zustand/Redux | Many consumers, frequent updates, app-wide state |
| URL params | Shareable, bookmarkable state |
| Server state | Data from API (use SWR/React Query) |
Common Mistakes
- Over-globalizing: Not everything belongs in global state
- Prop drilling overuse: Context is free, use it for deep trees
- Mutable state: Always treat state as immutable
- Storing derived data: Compute derived values inline, don't store them
Best Practices
- Keep state as close to where it's used as possible
- Prefer lifting state up over global state
- Use colocation — state, reducer, and component in same file
- For server data, use a dedicated library like TanStack Query
- Type your state with TypeScript
Next Steps
- Forms & Validation — Form state management
- Performance — Optimize re-renders
Exercise: Build a todo app using useReducer with add, toggle, and delete actions.
Key Takeaways
Local state (useState) should be the default — global state is only needed when multiple distant components share data
React Context is built-in but causes unnecessary re-renders when the value object changes frequently
Zustand provides minimal boilerplate global state with automatic render optimization via selectors
Redux scales well for large apps but requires disciplined patterns: slices, selectors, immutable updates
Server state (data from APIs) is fundamentally different from UI state and benefits from dedicated tools like TanStack Query
URL state is often the best place for filter, sort, and pagination state — it survives refreshes and is shareable