State Management (Conceptual)
TL;DR
Redux: Flux architecture with single immutable store, pure reducers, actions. Highly predictable, debuggable (time-travel), scales to large apps. Boilerplate-heavy.
MobX: Reactive, observable state with minimal boilerplate. Autotrack dependencies, auto-update. Elegant but subtle bugs can emerge.
Zustand: Lightweight Flux alternative. Hook-based, simple API, good performance. Popular for new projects.
Recoil: Facebook's fine-grained reactivity. Atoms (granular state), selectors (derived state). Still experimental.
Context API: React built-in. Good for small to medium apps, prop drilling avoidance, but re-renders not optimized for frequent updates.
Signals: Granular reactivity pattern (Solid.js, Angular). Direct dependency tracking without overhead.
Learning Objectives
You will be able to:
- Choose appropriate state management by app size, team experience, and complexity.
- Understand Flux architecture and uni-directional data flow benefits.
- Implement normalized state structures and selectors for performance.
- Recognize when state management is needed vs. over-engineering.
- Design state mutations and side effects handling.
- Monitor and debug state changes effectively.
Motivating Scenario
Your checkout flow has cart state (items, quantities, discounts), user state (profile, saved cards), UI state (modal open/closed, loading), and server state (order history, inventory).
Without proper state management:
- Cart stored in React component state (lost on navigation)
- User data fetched separately, duplicated across components
- Loading spinners managed ad-hoc (multiple true/false booleans)
- State mutations scattered everywhere (hard to trace)
Result: Race conditions (cart updates before user loads), stale data, hard to debug.
With Redux/Zustand:
- Single source of truth (all state in store)
- Predictable mutations (actions/reducers)
- Selectors for derived state (computed cart total)
- Time-travel debugging (see exact sequence of state changes)
- DevTools integration (inspect, replay)
The Problem Space
Why State Management Matters
As apps grow, state becomes scattered:
- Component local state (useState)
- Context API (shared across subtree)
- Server state (API responses)
- UI state (modals, loading)
- Session state (auth, user)
Without coordination:
- Race conditions (API response arrives after unmount)
- Stale data (user fetched once, never updated)
- Prop drilling (pass props 5 levels deep)
- Duplicate state (same data in multiple places)
- Hard debugging (where did this state come from?)
When You Need State Management
Don't use if:
- App is small (page or two)
- Minimal shared state
- Data rarely changes
Use if:
- Multiple features sharing state
- Complex state updates
- Server + client state syncing
- Need to debug state changes
- Team larger than 3 engineers
State Management Solutions
Context API (Built-in React)
No external library. Use React.createContext for sharing state across component tree.
- Simple Context
- Using Context
export const CartContext = createContext();
export function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addToCart = useCallback((product) => {
setItems(prev => [...prev, product]);
}, []);
const removeFromCart = useCallback((productId) => {
setItems(prev => prev.filter(p => p.id !== productId));
}, []);
return (
<CartContext.Provider value={{ items, addToCart, removeFromCart }}>
{children}
</CartContext.Provider>
);
}
export function Cart() {
const { items, addToCart, removeFromCart } = useContext(CartContext);
return (
<div>
<h2>Cart ({items.length})</h2>
{items.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => removeFromCart(item.id)}>Remove</button>
</div>
))}
</div>
);
}
Pros:
- Zero dependencies
- Easy to understand
- Works for simple cases
Cons:
- Re-renders entire subtree on change (performance issue)
- No built-in time-travel debugging
- Boilerplate for complex state
Use when: Small apps, theme/auth context, avoiding prop drilling.
Redux (Flux Architecture)
Centralized, immutable store with pure reducers. Industry standard for large apps.
- Redux Store
- Using Redux
- Redux Toolkit (Modern)
const initialState = {
cart: { items: [], total: 0 },
user: { id: null, name: '' },
ui: { loading: false, error: null },
};
function rootReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TO_CART':
return {
...state,
cart: {
items: [...state.cart.items, action.payload],
total: state.cart.total + action.payload.price,
},
};
case 'SET_LOADING':
return {
...state,
ui: { ...state.ui, loading: action.payload },
};
default:
return state;
}
}
export const store = createStore(rootReducer);
export function Cart() {
const dispatch = useDispatch();
const items = useSelector(state => state.cart.items);
const total = useSelector(state => state.cart.total);
const handleAddToCart = (product) => {
dispatch({
type: 'ADD_TO_CART',
payload: product,
});
};
return (
<div>
<h2>Cart ({items.length})</h2>
<p>Total: ${total.toFixed(2)}</p>
</div>
);
}
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: 0 },
reducers: {
addToCart: (state, action) => {
state.items.push(action.payload);
state.total += action.payload.price;
},
removeFromCart: (state, action) => {
state.items = state.items.filter(
item => item.id !== action.payload
);
},
},
});
export const { addToCart, removeFromCart } = cartSlice.actions;
export default cartSlice.reducer;
Pros:
- Single source of truth (single store)
- Pure reducers (predictable, testable)
- Time-travel debugging (Redux DevTools)
- Middleware ecosystem (async, logging, etc.)
Cons:
- Boilerplate (actions, action types, reducers)
- Steep learning curve
- Overkill for small apps
Use when: Large apps, complex state, need time-travel debugging.
Zustand (Lightweight)
Minimalist state management. Hook-based, simple API.
- Zustand Store
- Using Zustand
const useCartStore = create((set) => ({
items: [],
total: 0,
addToCart: (product) =>
set((state) => ({
items: [...state.items, product],
total: state.total + product.price,
})),
removeFromCart: (productId) =>
set((state) => ({
items: state.items.filter(p => p.id !== productId),
})),
clear: () => set({ items: [], total: 0 }),
}));
export default useCartStore;
export function Cart() {
const items = useCartStore((state) => state.items);
const addToCart = useCartStore((state) => state.addToCart);
return (
<div>
<h2>Cart ({items.length})</h2>
<button onClick={() => addToCart({ id: 1, price: 99 })}>
Add Item
</button>
</div>
);
}
Pros:
- Minimal boilerplate
- Hook-based, familiar React pattern
- Good performance (fine-grained re-renders)
- Small bundle size
Cons:
- Smaller ecosystem than Redux
- Less tooling/debugging
Use when: New projects, prefer simplicity over framework opinionation.
MobX (Reactive)
Observable state with automatic dependency tracking.
class CartStore {
items = [];
constructor() {
makeObservable(this, {
items: observable,
addToCart: action,
removeFromCart: action,
total: computed,
});
}
addToCart(product) {
this.items.push(product);
}
removeFromCart(productId) {
this.items = this.items.filter(p => p.id !== productId);
}
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
export const cartStore = new CartStore();
Pros:
- Minimal boilerplate
- Automatic dependency tracking
- Elegant API
Cons:
- "Magic" can hide bugs
- Less opinionated (easier to do wrong)
- Harder to debug
Use when: Team familiar with reactive paradigm, want minimal boilerplate.
Flux Architecture Deep Dive
Unidirectional data flow model:
User Action
↓
Dispatch Action
↓
Reducer (pure function)
↓
Updated State
↓
Components re-render
Key benefits:
- Predictability: Same action + state always produces same result
- Testability: Pure reducers easy to unit test
- Debuggability: Time-travel debugging (replay actions)
- Scalability: Clear separation of concerns
Patterns & Pitfalls
Pattern: Normalized State
Avoid nested state (hard to update). Use normalization:
// Bad: nested state
{
cart: {
items: [
{
id: 1,
product: { id: 'p1', name: 'Item', price: 99 },
quantity: 2,
},
],
},
}
// Good: normalized state
{
cart: {
itemIds: [1],
items: {
'1': { productId: 'p1', quantity: 2 },
},
},
products: {
'p1': { id: 'p1', name: 'Item', price: 99 },
},
}
Pattern: Selectors
Compute derived state:
// Selector: compute total from items
const selectCartTotal = (state) => {
return state.cart.itemIds.reduce((sum, itemId) => {
const item = state.cart.items[itemId];
const product = state.products[item.productId];
return sum + product.price * item.quantity;
}, 0);
};
// Use in component
const total = useSelector(selectCartTotal);
Pitfall: Over-Engineering
Problem: Using Redux for simple app with minimal shared state.
Mitigation: Start with Context API or Zustand. Graduate to Redux only if needed.
Pitfall: State Mutations
Problem: Directly mutating state (Redux reducer returns same state).
// Bad: mutates state
state.items.push(newItem);
return state; // Same reference!
// Good: create new state
return {
...state,
items: [...state.items, newItem],
};
Design Review Checklist
- Is state management solution appropriate for app complexity?
- Is state normalized (flat, not deeply nested)?
- Are selectors used to derive state (not derived in components)?
- Is loading/error state handled properly?
- Are mutations pure (no side effects in reducers)?
- Are async operations handled (middleware for side effects)?
- Is dev tooling configured (DevTools, logging)?
- Are selectors memoized for performance?
- Is state duplication avoided (single source of truth)?
- Can state be debugged (time-travel, logs)?
When to Use / When Not to Use
Use State Management When:
- Multiple features sharing state
- Complex state transitions
- Server + client state syncing
- Team larger than 3 engineers
- Need time-travel debugging
Skip If:
- Single feature, simple state
- Minimal shared state
- Small team
- Rapid prototyping
Showcase: Solution Comparison
Self-Check
- When would you use Redux over Zustand? What problems does Redux solve that Zustand doesn't?
- What's the benefit of normalized state? How would you structure a store with products and cart items?
- What is time-travel debugging? Which solutions support it?
Next Steps
- Redux Documentation ↗️
- Explore Zustand ↗️
- Learn about Architectural Patterns ↗️
- Study MobX ↗️
One Takeaway
Start simple: use Context API or React hooks. Graduate to Zustand or Redux only when shared state complexity warrants it. Prefer Flux (unidirectional) for large apps; reactive for elegant simplicity. Monitor your app's state evolution and refactor when pain points emerge.
References
- Redux: Predictable State Management
- Zustand: Lightweight State Management
- MobX: Observable State Management
- Recoil: Fine-Grained Reactive State
- Flux Architecture Pattern