> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 12. Redux & State Management

> Master global state management with Redux Toolkit.

# Redux & State Management

As your React application grows beyond a few components, you'll encounter challenges with state management. Props drilling becomes unwieldy, Context API re-renders too much, and tracking state changes becomes a nightmare. **Redux** addresses these problems with a structured approach to global state.

Think of Redux like a bank. Instead of every person (component) keeping cash under their mattress (local state) and passing envelopes of money to each other (props), everyone uses a single bank vault (the store). To deposit or withdraw, you fill out a form (dispatch an action). A teller (the reducer) processes your form and updates the ledger. Anyone can check their balance at any time (subscribe to state). The key insight: there is exactly one ledger, and every change goes through the same teller window, so you always have a clear audit trail.

## When Do You Need Redux?

Redux adds complexity, so it's important to know when it's worth it:

### You Might Need Redux When:

* **Multiple components** need access to the same state
* **State updates** come from many different sources (user input, API calls, WebSockets)
* **State changes** are complex with many interconnected pieces
* **Debugging** is difficult because you can't track what changed and when
* **Team collaboration** requires predictable patterns everyone follows

### You Probably Don't Need Redux When:

* Your app is simple with few components
* State is localized to individual components
* Context API handles your needs without performance issues
* You're building a prototype or MVP

<Tip>
  Start with React's built-in state management (`useState`, `useContext`). Only add Redux when you feel genuine pain from state complexity.
</Tip>

## Redux Core Concepts

Before diving into code, understand the philosophy:

| Concept      | Description                                                             | Analogy                                   |
| ------------ | ----------------------------------------------------------------------- | ----------------------------------------- |
| **Store**    | A single JavaScript object that holds all app state                     | The bank vault                            |
| **Action**   | A plain object describing what happened (`{ type: 'ADD_ITEM' }`)        | A filled-out form submitted to the teller |
| **Dispatch** | The function you call to send an action to the store                    | Handing the form to the teller            |
| **Reducer**  | A pure function that takes current state + action and returns new state | The teller processing your form           |
| **Selector** | A function that reads a specific slice of state                         | Checking your account balance             |

Three unbreakable rules:

1. **Single Source of Truth** -- All app state lives in one store object.
2. **State is Read-Only** -- The only way to change state is by dispatching actions. You never mutate the store directly.
3. **Pure Reducers** -- Changes are made with pure functions (given same input, always same output, no side effects).

This predictability makes Redux apps easier to debug, test, and reason about. Every state change has a traceable cause.

```
┌─────────────────────────────────────────────────────────────┐
│                     Redux Data Flow                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  UI (Component)                                             │
│       │  user clicks "Add to Cart"                          │
│       ▼                                                     │
│  dispatch({ type: 'ADD_ITEM', payload: item })              │
│       │                                                     │
│       ▼                                                     │
│  Reducer: (oldState, action) => newState                    │
│       │                                                     │
│       ▼                                                     │
│  Store updates                                              │
│       │                                                     │
│       ▼                                                     │
│  Subscribed components re-render with new state             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

## Redux Toolkit (RTK)

We'll use **Redux Toolkit (RTK)**, the official, recommended way to write Redux logic. It eliminates boilerplate and includes best practices by default.

## Installation

```bash theme={null}
npm install @reduxjs/toolkit react-redux
```

## 1. Create a Slice

A "slice" contains the reducer logic and actions for a single feature. Think of it like a department in the bank -- the counter department handles counting, the users department handles user accounts, etc. Each slice owns its own piece of state and the rules for changing it.

**features/counter/counterSlice.js**

```javascript theme={null}
import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
  // The name is used as a prefix for generated action types:
  // e.g., 'counter/increment', 'counter/decrement'
  name: 'counter',

  // The starting state for this slice
  initialState: {
    value: 0,
  },

  // Each key here becomes both an action creator AND a case in the reducer.
  // Redux Toolkit generates them automatically -- no more switch statements.
  reducers: {
    increment: (state) => {
      // This looks like mutation, but it is safe. Redux Toolkit uses the
      // Immer library under the hood, which intercepts these "mutations"
      // and produces a brand-new immutable state object behind the scenes.
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      // action.payload contains the value passed when dispatching:
      // dispatch(incrementByAmount(5)) --> action.payload === 5
      state.value += action.payload;
    },
  },
});

// RTK auto-generates action creators with the same names as the reducers
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// The reducer function is what we plug into the store
export default counterSlice.reducer;
```

<Warning>
  **Common pitfall -- "mutating" state outside of RTK**: The Immer magic only works inside `createSlice` and `createReducer`. If you write a plain reducer without RTK, you must return a new state object the old-fashioned way (`return { ...state, value: state.value + 1 }`). Mutating state in a plain reducer will cause silent bugs where components do not re-render.
</Warning>

## 2. Configure the Store

Create the Redux store and add your slices. The store is the single source of truth for your entire application's state. Think of `configureStore` as assembling a filing cabinet -- each drawer (slice) holds documents for one department, and the cabinet itself is the store.

**app/store.js**

```javascript theme={null}
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  // Each key here becomes a top-level key in the state tree.
  // state.counter.value comes from the counter slice.
  // Add more slices as your app grows: users, cart, notifications, etc.
  reducer: {
    counter: counterReducer,
  },
  // configureStore automatically adds Redux DevTools integration
  // and the thunk middleware for async logic. You get these for free.
});
```

## 3. Provide the Store

Wrap your application with the `Provider`.

**main.jsx**

```javascript theme={null}
import { store } from './app/store';
import { Provider } from 'react-redux';

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <App />
  </Provider>
);
```

## 4. Use State and Dispatch

Use `useSelector` to read data from the store and `useDispatch` to send actions. These two hooks are the bridge between your React components and the Redux world.

**Counter.jsx**

```javascript theme={null}
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './features/counter/counterSlice';

export function Counter() {
  // useSelector extracts a value from the store.
  // It subscribes to the store and re-renders this component
  // ONLY when the selected value changes -- not on every store update.
  const count = useSelector((state) => state.counter.value);

  // useDispatch returns the store's dispatch function.
  // Call it with an action creator to trigger a state change.
  const dispatch = useDispatch();

  return (
    <div>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <span>{count}</span>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}
```

<Tip>
  **Performance tip**: The selector function you pass to `useSelector` determines when the component re-renders. Keep selectors focused -- select only the data you need. If you select the entire state object (`state => state`), the component re-renders on *every* store change.
</Tip>

## Async Logic (Thunks)

Redux reducers must be pure and synchronous -- they cannot make API calls. So how do you fetch data? Redux Toolkit includes `createAsyncThunk`, which handles the async work and automatically dispatches `pending`, `fulfilled`, and `rejected` actions for you.

Think of it like ordering food: you place the order (dispatch the thunk), the kitchen works on it (async API call), and eventually you either get your meal (fulfilled) or an apology (rejected). The reducer just needs to react to each of those three outcomes.

```javascript theme={null}
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

// createAsyncThunk generates three action types automatically:
// 'users/fetchById/pending'   -- dispatched when the request starts
// 'users/fetchById/fulfilled' -- dispatched when the request succeeds
// 'users/fetchById/rejected'  -- dispatched when the request fails
export const fetchUser = createAsyncThunk(
  'users/fetchById',
  async (userId, thunkAPI) => {
    const response = await fetch(\`/api/users/\${userId}\`);
    if (!response.ok) throw new Error('Failed to fetch user');
    return response.json();
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], loading: 'idle', error: null },
  // extraReducers handles actions defined outside this slice,
  // including the auto-generated thunk actions.
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = 'loading';
        state.error = null;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = 'idle';
        state.entities.push(action.payload);
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = 'idle';
        state.error = action.error.message;
      });
  },
});
```

<Note>
  **Why three states?** Tracking `pending`, `fulfilled`, and `rejected` separately lets you show loading spinners, success messages, and error alerts in your UI -- all driven by a single dispatched thunk. This is much cleaner than managing `isLoading` booleans manually.
</Note>

## Common Redux Pitfalls

<Warning>
  **Pitfall 1 -- Storing server state in Redux**: Redux is designed for client state (UI toggles, form data, user preferences). For server state (API responses, cached data, pagination), libraries like React Query or SWR are purpose-built and handle caching, refetching, and stale data automatically. Stuffing API data into Redux means you are manually re-implementing what these libraries do out of the box.

  **Pitfall 2 -- Selecting too much state**: If your `useSelector` returns a new object every time, the component re-renders on every store update -- even if the data it cares about has not changed.

  ```javascript theme={null}
  // BAD: creates a new object every render, so React always sees "new" props
  const userData = useSelector((state) => ({
    name: state.user.name,
    email: state.user.email,
  }));

  // GOOD: select primitives individually, or use a memoized selector
  const name = useSelector((state) => state.user.name);
  const email = useSelector((state) => state.user.email);
  ```

  **Pitfall 3 -- Mutating state outside of createSlice**: Immer's safe "mutation" syntax only works inside `createSlice` reducers and `createReducer`. If you write a plain reducer or mutate state anywhere else, React will not detect the change and the UI will not update. This is a silent bug that is extremely hard to track down.

  **Pitfall 4 -- Putting non-serializable values in the store**: Redux expects the store to be serializable (plain objects, arrays, strings, numbers, booleans). Putting class instances, functions, Promises, or Date objects into the store breaks time-travel debugging and persist/rehydrate workflows. Store ISO strings instead of Date objects, and keep functions out of state.
</Warning>

## Other Options

While Redux is popular, other libraries exist:

* **Zustand**: Minimalist, hook-based state management.
* **Recoil / Jotai**: Atomic state management.
* **TanStack Query (React Query)**: Best for server state (caching API responses).

***

## Redux vs. Context API vs. Other Options

Choosing the right state management tool matters. Here is a practical comparison:

| Criteria              | Context API              | Redux Toolkit                  | Zustand             | React Query      |
| --------------------- | ------------------------ | ------------------------------ | ------------------- | ---------------- |
| **Best for**          | Theme, auth, locale      | Complex client state           | Simple global state | Server/API state |
| **Boilerplate**       | Low                      | Medium (RTK reduces it)        | Very low            | Low              |
| **DevTools**          | None built-in            | Excellent time-travel debugger | Basic               | Excellent        |
| **Re-render control** | Re-renders all consumers | Granular with selectors        | Granular            | Automatic        |
| **Async support**     | Manual                   | `createAsyncThunk`             | Manual              | Built-in         |
| **Learning curve**    | Low                      | Medium                         | Low                 | Low              |

<Tip>
  **Practical rule of thumb**: Use `useState` and `useContext` until you feel pain. If your state logic has many interconnected actions (think: e-commerce cart with coupons, taxes, shipping), Redux gives you structure and debugging tools that pay for themselves. For server state (API data, caching, pagination), skip Redux entirely and use React Query or SWR.
</Tip>

***

## Summary

| Concept              | Description                                                                     |
| -------------------- | ------------------------------------------------------------------------------- |
| **Store**            | Single object holding all application state                                     |
| **Slice**            | A feature-sized bundle of reducer + actions (RTK)                               |
| **Action**           | A plain object describing what happened                                         |
| **Reducer**          | A pure function that computes the next state                                    |
| **Provider**         | Makes the store available to the component tree                                 |
| **useSelector**      | Reads (subscribes to) a piece of state                                          |
| **useDispatch**      | Sends actions to trigger state updates                                          |
| **createAsyncThunk** | Handles async operations with auto-generated pending/fulfilled/rejected actions |

<Card title="Next Steps" icon="arrow-right">
  In the next chapter, you'll learn about **Custom Hooks & Advanced Patterns** -- extracting reusable logic into hooks you can share across your entire app!
</Card>

***

## Interview Deep-Dive

<AccordionGroup>
  <Accordion title="Why must Redux reducers be pure functions? What would break if a reducer had side effects?">
    **Strong Answer:**
    Redux's value proposition rests on predictability: given the same state and action, a reducer must return the same new state. This enables time-travel debugging (replaying actions produces identical states), hot module reloading, and simple testing.

    If a reducer made an API call, replaying the action in DevTools would trigger another call -- possibly charging a customer twice. If it used `Math.random()` or `Date.now()`, replay produces different state. If it mutated existing state instead of returning a new object, the `===` check for state changes fails and components do not re-render.

    Redux Toolkit's `configureStore` includes middleware that checks for accidental mutations and non-serializable values in development, throwing errors early.

    **Follow-up: Where DO side effects go in Redux?**

    Thunks are async functions that receive dispatch and getState. `createAsyncThunk` generates pending/fulfilled/rejected actions automatically. Simple and sufficient for most apps. Sagas use generator functions for complex orchestration like racing actions or debouncing. RTK Query auto-generates hooks with caching, deduplication, and optimistic updates for server state. My recommendation: RTK Query for server data, thunks for occasional client async, sagas only for genuinely complex orchestration.
  </Accordion>

  <Accordion title="How does useSelector work under the hood? What is the performance difference between selecting entire state versus a specific slice?">
    **Strong Answer:**
    `useSelector` subscribes to the store via `store.subscribe()`. On every dispatch, the subscription callback runs the selector function and compares the result to the previous result using `===`. If changed, the component re-renders.

    Selecting entire state -- `useSelector(state => state)` -- returns the root object, which is a new reference on every dispatch. The component re-renders on every store update, defeating the purpose of selectors.

    Selecting a specific slice -- `useSelector(state => state.counter.value)` -- returns a primitive. Unrelated slice changes do not affect this value, so the component skips rendering.

    The subtle case: `useSelector(state => state.items.filter(i => i.active))` creates a new array every call. Use `createSelector` from Reselect to memoize derived data.

    **Follow-up: How does createSelector differ from useMemo?**

    `createSelector` is a pure function called outside React's render cycle -- usable in tests, shared across components, composable. `useMemo` is a hook tied to a component instance. `createSelector` has a cache size of 1 by default, so parameterized selectors may need a factory pattern or larger cache.
  </Accordion>

  <Accordion title="You join a team that uses Redux for everything -- API caching, form state, UI toggles. What would you change?">
    **Strong Answer:**
    I would categorize the state and migrate each to the right tool. Server state moves to React Query or RTK Query for caching, deduplication, and background refetching. Form state moves to component-local state or React Hook Form -- dispatching an action on every keystroke adds latency for zero benefit. UI state (modal open/closed) moves to local useState.

    What stays in Redux: truly global client state read by many unrelated components with complex update logic benefiting from DevTools. Authentication, user preferences, shopping cart with pricing rules.

    The migration approach: start with highest-churn Redux state (forms, API data) first. Each migration reduces dispatches per second, improving performance.

    **Follow-up: How do you manage the transition period with some state in Redux and some in React Query?**

    Run them side by side with both Providers. Migrate one endpoint at a time: replace the thunk, slice, and selector with a useQuery hook. Components use both hooks where needed: `useQuery` for server data and `useSelector` for client state. No bridging required. The only complication is Redux middleware orchestrating flows that mix server and client data -- restructure those as React hooks composing useQuery with useSelector.
  </Accordion>
</AccordionGroup>
