# Redux Toolkit

## 1. The Same Model, Radically Less Boilerplate

Redux Toolkit (RTK) is the officially recommended way to write Redux — it does not replace Redux's model from the previous topic (single store, dispatched actions, pure reducers), it generates the boilerplate that model requires. Everything RTK produces is still plain Redux underneath: a `createSlice` call still ends up as an ordinary reducer function and ordinary action creators, usable with plain `useSelector`/`useDispatch` exactly as before.

```ts
// Plain Redux (from the previous topic): 3 separate things to write and keep in sync
const INCREMENT = "counter/increment";
function increment() { return { type: INCREMENT }; }
function counterReducer(state = { count: 0 }, action: any) {
  switch (action.type) {
    case INCREMENT: return { count: state.count + 1 };
    default: return state;
  }
}
```

```ts
// Redux Toolkit: ONE createSlice call generates the reducer AND the action creator together
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { count: 0 },
  reducers: {
    increment: (state) => { state.count += 1; }, // looks like mutation — see section 2
  },
});

export const { increment } = counterSlice.actions; // auto-generated action creator
export default counterSlice.reducer;                 // auto-generated reducer
```

## 2. `createSlice` and the Immer-Powered "Mutating" Syntax

The previous topic was emphatic: reducers must never mutate state. RTK's `createSlice` reducer functions *look* like they violate that rule (`state.count += 1`) — but they don't, because `createSlice` wraps every reducer function with **Immer**, a library that lets you write code that *appears* to mutate a draft object, while actually producing a correct, brand-new immutable state object behind the scenes.

```ts
const todosSlice = createSlice({
  name: "todos",
  initialState: [] as { id: number; text: string; done: boolean }[],
  reducers: {
    add: (state, action: { payload: { id: number; text: string } }) => {
      state.push({ id: action.payload.id, text: action.payload.text, done: false }); // looks like a mutating .push()
    },
    toggle: (state, action: { payload: number }) => {
      const todo = state.find((t) => t.id === action.payload);
      if (todo) todo.done = !todo.done; // looks like a direct property mutation
    },
  },
});
```

Compare this to the previous topic's hand-written `todosReducer`, which had to spread arrays and objects explicitly (`[...state, newTodo]`, `{...t, done: !t.done}`) to avoid mutating. Immer lets the reducer body read like the "obviously correct, obviously wrong for Redux" version, while Immer's proxy-based tracking produces the properly immutable result — the payoff is large for deeply nested state, where hand-written immutable updates get genuinely hard to read correctly.

## 3. `configureStore`: Sensible Defaults, One Function Call

```ts
import { configureStore } from "@reduxjs/toolkit";

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    todos: todosSlice.reducer,
  },
});
```

`configureStore` replaces `createStore` + `combineReducers` + manually calling `applyMiddleware(thunk)` + manually wiring Redux DevTools — it does all of that automatically: thunk middleware is included by default, DevTools integration is enabled by default (in development), and it adds development-only middleware that warns on accidental state mutation outside a slice reducer or non-serializable values landing in the store, catching classes of bugs plain `createStore` would let through silently.

## 4. `createAsyncThunk`: Async Logic Without Hand-Rolled Loading States

```ts
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

const fetchUser = createAsyncThunk("user/fetch", async (userId: string) => {
  const res = await fetch(`/api/users/${userId}`);
  return res.json(); // becomes action.payload in the fulfilled case
});

const userSlice = createSlice({
  name: "user",
  initialState: { data: null as any, status: "idle" as "idle" | "loading" | "succeeded" | "failed" },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => { state.status = "loading"; })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.data = action.payload;
      })
      .addCase(fetchUser.rejected, (state) => { state.status = "failed"; });
  },
});

dispatch(fetchUser("42")); // dispatches pending immediately, then fulfilled or rejected
```

`createAsyncThunk` takes a type-prefix string and an async payload-creator function, and automatically generates **three** action types around it (`pending`, `fulfilled`, `rejected`) dispatched at the right moments of the Promise's lifecycle — this is the same underlying thunk-middleware mechanism from the previous topic's Part 2, but with the loading/success/error action bookkeeping generated for you instead of hand-written per async operation.

## 5. Contrast Directly With Plain Redux's Boilerplate

```
Plain Redux, adding ONE new capability (e.g. "toggle a todo"):
  1. add a type constant
  2. write an action creator function
  3. add a case branch to the reducer's switch, with manual immutable spreading

Redux Toolkit, the same capability:
  1. add one key to the slice's `reducers` object, mutate the draft directly
     (action creator + action type string are both generated automatically)
```

The reduction isn't just fewer lines — it's fewer *places* a single logical change has to touch, which is exactly the kind of boilerplate that grows error-prone (forgetting to add the switch case, a typo in a type-constant string used in two places) as an app's slice count grows.

## Under the Hood

**Immer works by wrapping the state you pass in a JavaScript Proxy, recording every property access and assignment made against it, and only ever handing Redux a real, newly-constructed object.** When a `createSlice` reducer runs, RTK calls it with a Proxy standing in for the real `state` — every `state.count += 1` or `state.push(...)` you write is actually intercepted by the Proxy's trap handlers, which record "this path was written to, with this new value" rather than mutating anything real. Once your reducer function returns, Immer walks its recorded changes and **produces a brand-new object graph**, structurally sharing (reusing the same references for) every part of the state tree that wasn't touched — so a `toggle` reducer that flips one todo's `done` flag still returns a new todos array with a new object for that one todo, but every *other* todo object in that array is the exact same reference as before, exactly matching what a hand-written immutable update would have produced.

**This is why the "mutating" syntax is safe ONLY inside a `createSlice` reducer, and dangerous anywhere else.** The Proxy-and-draft machinery is set up by RTK specifically around the function you pass to `reducers`; mutating a plain object you pulled out of `useSelector()` or a state value sitting in a `useState` elsewhere in your app is a real, ordinary JavaScript mutation with none of Immer's safety net — RTK does not globally intercept every object mutation in your codebase, only the ones happening inside its own reducer-wrapping mechanism.

![Immer runs the reducer against a Proxy draft and produces a new state that copies only the changed path while sharing untouched branches](diagrams/01-immer-structural-sharing.svg)

## Common Pitfalls

1. **Mutating state OUTSIDE a `createSlice` reducer** (e.g. directly mutating a value pulled from `useSelector`) expecting Immer to catch it — Immer's proxy wrapping only applies inside the reducer function RTK controls; mutating elsewhere is a real, unguarded mutation.
2. **Returning a new value AND mutating the draft in the same reducer** — Immer supports either pattern (mutate the draft, or return a brand-new value) but mixing both in one reducer produces a runtime error, since Immer can't reconcile "you changed the draft" with "you also returned something else."
3. **Forgetting `extraReducers` for `createAsyncThunk`'s generated action types** — defining the thunk alone does nothing to state; a slice still needs `.addCase(thunk.pending/fulfilled/rejected, ...)` wiring to actually react to it.
4. **Assuming `configureStore`'s default middleware includes everything a large app needs** — its serializability/immutability dev-mode checks are valuable but add runtime overhead; some teams explicitly narrow them for performance-sensitive production builds via the `middleware` option.
5. **Writing a new slice's `reducers` as if starting from scratch each time**, duplicating async-status boilerplate (`status: "idle"|"loading"|...`) by hand across many slices instead of extracting a small shared helper — a common RTK-specific pattern once several slices need the same pending/fulfilled/rejected shape.

## Quick Reference

```
createSlice({ name, initialState, reducers, extraReducers })
  → generates BOTH action creators (slice.actions.x) and the reducer (slice.reducer)
  → reducer bodies can "mutate" the draft directly — Immer produces the real immutable update

configureStore({ reducer: { ... } })
  → replaces createStore + combineReducers + applyMiddleware(thunk) + DevTools wiring,
    all with sensible defaults out of the box

createAsyncThunk(typePrefix, asyncPayloadCreatorFn)
  → auto-generates pending/fulfilled/rejected action types around a Promise
  → wire them up in a slice's extraReducers via builder.addCase(...)

Immer mechanism: Proxy-wrapped draft records writes, produces a NEW object graph on return,
                 structurally shares every untouched part of the tree (same references)

Still plain Redux underneath: one store, dispatched actions, pure reducers — RTK only
removes the boilerplate around writing them, per Part 2's closing note in the Redux topic.
```
