Skip to main content

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.

CartContext.jsx

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>
);
}

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.

store.js

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);

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.

store.js

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;

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.

store.js

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:

  1. Predictability: Same action + state always produces same result
  2. Testability: Pure reducers easy to unit test
  3. Debuggability: Time-travel debugging (replay actions)
  4. 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

  1. When would you use Redux over Zustand? What problems does Redux solve that Zustand doesn't?
  2. What's the benefit of normalized state? How would you structure a store with products and cart items?
  3. What is time-travel debugging? Which solutions support it?

Next Steps

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

  1. Redux: Predictable State Management
  2. Zustand: Lightweight State Management
  3. MobX: Observable State Management
  4. Recoil: Fine-Grained Reactive State
  5. Flux Architecture Pattern