RoadmapDay 26 / 80
React (Web)Month 2 · Week 6

Day 26: Global State Architecture: Redux Toolkit Mechanics

Understand the unidirectional data flow Redux enforces and why Redux Toolkit's "mutating" reducers are actually safe under the hood.

Mark this day complete

Study

Concepts

One store, pure reducers, unidirectional flow

Redux holds ALL global state in a single store. Updates only happen by dispatching a plain-object `action` (`{ type, payload }`) to a `reducer`, a pure function `(state, action) => newState` that must never mutate its input and must produce the same output for the same input every time. The flow is strictly one-directional: UI dispatches an action → reducer computes new state → store notifies subscribers → UI re-renders from the new state — never the reverse, which is what makes state changes traceable and debuggable (every change has a named action in the history).

Redux Toolkit (RTK) is the modern, official way to write Redux — `createSlice` generates action creators and a reducer from a single object of "case reducers", and internally uses Immer so you can write code that LOOKS like direct mutation (`state.count += 1`) while Immer actually produces a new immutable state object behind a Proxy (the exact mechanism from Day 10) — you get the ergonomics of mutation with the correctness guarantees of immutability.

Middleware and async logic

Reducers must stay synchronous and side-effect-free, so async logic (API calls) lives in middleware. RTK's `createAsyncThunk` wraps an async function and automatically dispatches `pending` / `fulfilled` / `rejected` actions around it, so a reducer can react to each stage (e.g. set a loading flag on `pending`, store data on `fulfilled`) without ever performing the fetch itself.

Selectors (plain functions that read a slice of state, often memoized with `createSelector` from Reselect) are the read-side counterpart to actions — they decouple components from the shape of the store, so the store's internal structure can change without touching every component that reads from it.

See It

Visualizations

Visualization

Unidirectional Redux data flow

UI event

dispatch({ type: "cart/add", payload })

Middleware

thunks handle async side effects here

Reducer

pure function computes the next state

Store updates

notifies all subscribers

UI re-renders

connected components read new state via selectors

Build It

Code Examples

A cart slice with createSlice + createAsyncThunk

js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchCart = createAsyncThunk('cart/fetch', async (userId) => {
  const res = await fetch(`/api/cart/${userId}`);
  return res.json(); // becomes action.payload on fulfilled
});

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [], status: 'idle', error: null },
  reducers: {
    // Looks like mutation — Immer converts this into an immutable update
    itemAdded(state, action) {
      state.items.push(action.payload);
    },
    itemRemoved(state, action) {
      state.items = state.items.filter((i) => i.id !== action.payload.id);
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchCart.pending, (state) => { state.status = 'loading'; })
      .addCase(fetchCart.fulfilled, (state, action) => {
        state.status = 'idle';
        state.items = action.payload;
      })
      .addCase(fetchCart.rejected, (state, action) => {
        state.status = 'error';
        state.error = action.error.message;
      });
  },
});

export const { itemAdded, itemRemoved } = cartSlice.actions;
export default cartSlice.reducer;

A memoized selector with Reselect

js
import { createSelector } from '@reduxjs/toolkit';

const selectItems = (state) => state.cart.items;

// Recomputes ONLY when 'items' actually changes — not on every store update
export const selectCartTotal = createSelector([selectItems], (items) =>
  items.reduce((sum, item) => sum + item.price * item.qty, 0)
);

Remember

Key Takeaways

  • Redux enforces one direction: dispatch → reducer (pure) → store → re-render — never mutate state outside a reducer.
  • RTK's createSlice uses Immer under a Proxy, so "mutating" syntax produces safe, immutable updates.
  • Async work belongs in middleware/thunks — reducers stay synchronous and side-effect-free.
  • createAsyncThunk auto-dispatches pending/fulfilled/rejected — model loading and error state from those three cases.
  • Memoized selectors (createSelector) decouple components from store shape and avoid recomputing derived data unnecessarily.

Do It

Practice

  1. 1Build a cartSlice with add/remove/clear reducers and a fetchCart thunk, wired to a real or mocked endpoint.
  2. 2Add a memoized selectCartTotal and prove with a console.log inside it that it does NOT recompute when an unrelated slice of state changes.
  3. 3Open Redux DevTools and step through the action history to time-travel to an earlier state.