Redux Architecture Overview
The whole Redux loop on one page - UI, action, dispatch, middleware, reducers, store, selectors - traced step by step through a real click and a real server request. Then how to draw a feature on paper before writing code.
What you will be able to do
- Name every part of the Redux loop and what each one does
- Follow one click through the loop, in the real order, from a logged trace
- Explain why every reducer is called for every action
- Describe the shape of the state: one root object made of slices
- Follow the async path: a thunk, the server, and the pending and fulfilled actions
- Plan a feature on paper - actions, state, selectors, components - before coding it
- Know which module of this course covers each part
The idea, in plain English
Lessons 1 to 8 were about the problem: why state gets hard, and when Redux is worth it. From here on the course is about Redux itself. Before learning each part in detail (Modules 2 to 10), it helps to see the whole machine once, from far away - "the shape before the detail", as the outline says.
Redux has one store that holds the app’s shared state, and one direction in which data moves. The UI never changes the state directly. It dispatches an action - a plain object that says what happened. The action passes through middleware, then the reducers calculate the new state, the store saves it and tells its subscribers, and selectors give each component the piece it shows. Then the user does something again, and the loop repeats.
In this lesson we do not just draw that loop - we run it. Every part writes a line to a log, so you can see the real order of events for one click and for one server request. All code was run with Redux Toolkit 2.13.0, react-redux 9.3.0 and React 19.3, rendered and clicked in a test browser (jsdom).
Worked example: Drawing the loop on paper first: a "Save lesson" feature planned in a table, then written in 30 lines.
1 - The user clicks, the UI dispatches
The click handler does not change any state. It describes what happened - an action, { type: "cart/added", payload: "Docker" } - and hands it to dispatch.
One click on "Cart", step by step, in the order our logged trace printed it. After the last box, the loop starts again at the top with the next click.
Reducers must be pure, so they cannot call a server. A thunk does the waiting, then dispatches ordinary actions with the result.
An everyday picture: the bank counter
In a bank you cannot walk into the vault and change your balance. You fill in a slip - "deposit 500" - and hand it in at the counter. A security guard looks at every slip first. Then a clerk, following the bank’s rules, updates the ledger. The ledger is the one true record. Later, when you look at your passbook or the app, you see only your own account, not the whole ledger.
Redux is the same. The slip is the action. Handing it in is dispatch. The guard is middleware. The clerk following rules is the reducer. The ledger is the store. Looking at only your account is a selector. And just like in the bank, you never write in the ledger yourself.
The deposit slipAn action: a description of what happened.Handing it indispatch(action).The security guardMiddleware: sees every slip before the clerk.The clerk and the rulesReducers: calculate the new state from the old state and the action.The ledgerThe store: the one true record.Your passbook viewA selector: only the part you need.The parts, on one page
These are all the parts of the loop. Lesson 10 defines each term carefully; here, just see where each one sits.
UI (components)Shows state, and dispatches actions when something happens.ActionA plain object: { type, payload }. Describes what happened.dispatchThe only way to send an action to the store.MiddlewareSees every action before the reducers: logging, async, analytics.ReducersPure functions: (old state, action) -> new state.StoreHolds the state, runs the reducers, notifies subscribers.SelectorsFunctions that read a piece of state for a component.ThunkAn async function you can dispatch; it talks to servers and dispatches actions.One direction
Follow the arrows in the first diagram: they go round in one direction only. The UI never writes to the store. The reducers never touch the UI. Middleware never changes state. Each part has one job, and talks only to the next part.
This is what makes Redux predictable. If the state is wrong, there are only two questions: which action was dispatched, and what did the reducer do with it? Lesson 8’s replay example answered both from a list of actions. You never have to search the whole app for "who changed this".
Example 1: trace one click
This app has one button and a store with two parts: a cart slice and a small user reducer. Every part of the loop writes a numbered line to a log. We clicked the button once and printed the log.
Three things in the output surprise most people. First, the user reducer was called, although the action was about the cart. The store calls every reducer for every action; each reducer decides whether the action concerns it, and if not, returns its state unchanged. Second, the selector ran twice: once when the store notified react-redux (to check whether the value changed), and once while CartButton rendered. Third, nothing in the UI code changed the cart - the click only described what happened.
import { configureStore, createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";
export const log = [];
const step = (text) => log.push(text);
// ---- a slice: owns one part of the state ----
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] },
reducers: {
added(state, action) {
step("4. cart reducer handles " + action.type);
state.items.push(action.payload);
},
},
extraReducers: (builder) => {
builder.addCase(loadCart.fulfilled, (state, action) => {
step("4. cart reducer handles " + action.type);
state.items = action.payload;
});
},
});
// ---- another part of the state, written as a plain reducer function ----
function userReducer(state = { name: "Ravi" }, action) {
step("4. user reducer is called too - returns the same state");
return state;
}
// ---- async: a thunk talks to the server, then dispatches the result ----
const fakeServer = async () => ["Python", "Redux"];
export const loadCart = createAsyncThunk("cart/load", async () => {
step("A. thunk calls the server");
const items = await fakeServer();
step("B. server answered");
return items;
});
// ---- middleware: sees every action before the reducers ----
const logger = () => (next) => (action) => {
step("3. middleware sees " + action.type);
return next(action);
};
// ---- the store: ONE root reducer made from the parts ----
export const store = configureStore({
reducer: { cart: cartSlice.reducer, user: userReducer },
middleware: (getDefault) => getDefault().concat(logger),
});
store.subscribe(() => step("5. store notifies subscribers"));
// ---- the UI: selects what it shows, dispatches what happened ----
function CartButton() {
const count = useSelector((state) => {
step("6. selector runs");
return state.cart.items.length;
});
const dispatch = useDispatch();
step("7. CartButton renders: " + count + " items");
return (
<button
onClick={() => {
step("1. user clicks");
step("2. dispatch(cart/added)");
dispatch(cartSlice.actions.added("Docker"));
}}
>
Cart ({count})
</button>
);
}
export default function App() {
return (
<Provider store={store}>
<CartButton />
</Provider>
);
}1. user clicks
2. dispatch(cart/added)
3. middleware sees cart/added
4. cart reducer handles cart/added
4. user reducer is called too - returns the same state
5. store notifies subscribers
6. selector runs <- react-redux checks: did my value change?
6. selector runs <- again, while CartButton renders
7. CartButton renders: 1 items
screen: Cart (1)The shape of the state
There is one store, so there is one state object. configureStore({ reducer: { cart, user } }) builds it: each key gets its value from its own reducer. After the click and the load below, store.getState() printed {"cart":{"items":["Python","Redux"]},"user":{"name":"Ravi"}}.
Each slice owns one key and nothing else. The cart reducer only ever receives state.cart; it cannot see or change state.user. This keeps each part small and lets a team work on different slices without stepping on each other. Module 12 (Redux State Architecture) is about designing this shape well.
Example 2: the async path
Reducers must be pure functions: same input, same output, no waiting, no server calls. So where does fetching data go? Into a thunk - an async function you dispatch like an action. createAsyncThunk builds one, and it dispatches ordinary actions for you as the request moves along.
We dispatched loadCart() and printed the log. The middleware first saw cart/load/pending. No reducer handles pending here, so the state did not change - and react-redux did not even run the selector. Then the thunk called the server, waited, and when the answer came, cart/load/fulfilled went through the normal loop: middleware, reducers, notify, selectors, render. The screen changed to Cart (2). The thunk itself never reaches a reducer; only the plain actions it dispatches do. Module 8 (Async Redux) covers this path in depth.
3. middleware sees cart/load/pending
4. user reducer is called too - returns the same state
5. store notifies subscribers (state unchanged: no selector, no render)
A. thunk calls the server
B. server answered
3. middleware sees cart/load/fulfilled
4. cart reducer handles cart/load/fulfilled
4. user reducer is called too - returns the same state
5. store notifies subscribers
6. selector runs
6. selector runs
7. CartButton renders: 2 items
screen: Cart (2)
store.getState() -> {"cart":{"items":["Python","Redux"]},"user":{"name":"Ravi"}}Draw the loop on paper first
This is the example from the course outline, and a habit worth building now. Before writing any Redux code for a feature, fill in a small table: what happens (actions), what the state looks like (shape), how each action changes it, what the UI needs to ask (selectors), and which components dispatch and select.
Here is the plan for a "Save lesson" feature, like the bookmark on this site. Notice that the plan already answers the hard questions - for example, "what if the user saves the same lesson twice?" - before any code exists. Writing the code afterwards is mostly copying the table.
import { configureStore, createSlice } from "@reduxjs/toolkit";
// From the paper plan: state shape, two actions, two selectors
const savedSlice = createSlice({
name: "saved",
initialState: { ids: [] }, // shape: { ids: ["redux-1", ...] }
reducers: {
lessonSaved(state, action) { // payload: a lesson id
if (!state.ids.includes(action.payload)) state.ids.push(action.payload);
},
lessonUnsaved(state, action) {
state.ids = state.ids.filter((id) => id !== action.payload);
},
},
});
export const { lessonSaved, lessonUnsaved } = savedSlice.actions;
// selectors: the questions the UI asks
export const selectSavedCount = (state) => state.saved.ids.length;
export const selectIsSaved = (state, id) => state.saved.ids.includes(id);
const store = configureStore({ reducer: { saved: savedSlice.reducer } });
store.dispatch(lessonSaved("redux-1"));
store.dispatch(lessonSaved("redux-2"));
store.dispatch(lessonSaved("redux-1")); // saving twice changes nothing
store.dispatch(lessonUnsaved("redux-2"));
const state = store.getState();
console.log("state:", JSON.stringify(state));
console.log("header badge -> selectSavedCount:", selectSavedCount(state));
console.log("save button -> selectIsSaved('redux-1'):", selectIsSaved(state, "redux-1"));
console.log("save button -> selectIsSaved('redux-2'):", selectIsSaved(state, "redux-2"));state: {"saved":{"ids":["redux-1"]}}
header badge -> selectSavedCount: 1
save button -> selectIsSaved('redux-1'): true
save button -> selectIsSaved('redux-2'): falseState shapesaved: { ids: ["redux-1", ...] } - only ids, the lessons themselves are server data.Action: lessonSaved(id)Adds the id - but only if it is not already there.Action: lessonUnsaved(id)Removes the id.Selector: selectSavedCountThe header badge asks: how many are saved?Selector: selectIsSaved(id)Each Save button asks: is this lesson saved?ComponentsSaveButton dispatches lessonSaved / lessonUnsaved; HeaderBadge and SaveButton select.Where the files go
The Redux documentation recommends organising code by feature, not by type: all the Redux logic for one feature in one slice file, next to that feature’s components. One store file creates the store from all the slices. You will build exactly this structure in Module 4.
src/
app/
store.js configureStore({ reducer: { cart, saved, user } })
features/
cart/
cartSlice.js state, reducers, actions, selectors for the cart
CartButton.jsx
saved/
savedSlice.js
SaveButton.jsx
main.jsx <Provider store={store}><App /></Provider>The map of this course
Every part of the loop has its own module. When a later lesson feels detailed, come back to the first diagram and find where that part sits.
Store, actions, reducersModule 2 (Redux Fundamentals), Module 3 (without Toolkit), Module 4 (Redux Toolkit)UI connection (Provider, useSelector, useDispatch)Module 5 (Redux with React)Actions and payloadsModule 6SelectorsModule 7The async path (thunks)Module 8 (Async Redux)MiddlewareModule 9Server dataModule 10 (RTK Query)The shape of the stateModule 12 (Redux State Architecture)Common questions
Why call every reducer for every action? Because an action describes something that happened, and more than one part of the state may care. "User logged out" might clear the cart, the saved lessons and the notifications at once. Each slice decides for itself; reducers that do not care return their state unchanged, which is very fast.
Can a component change the store without dispatch? It should never try. Redux Toolkit freezes the state (Lesson 1), so a direct write throws in an ES module - and even if it did not, nothing would be notified.
Is middleware required? configureStore adds a few useful ones by default, including the thunk middleware that makes dispatch(loadCart()) work, and development checks. You add your own - like our logger - only when you need them.
Why is the selector run twice? Once to decide whether the component must re-render, once during the render itself. Selectors should therefore be fast and have no side effects. Module 7 shows how to make expensive ones cheap.
The loop at a glance
1. UI dispatchesDescribe what happened.
dispatch(cartSlice.actions.added("Docker"))2. MiddlewareSees every action first.
(store) => (next) => (action) => next(action)
3. ReducersAll are called; each returns its part of the new state.
(state, action) => newState
4. StoreSaves the new state, notifies subscribers.
store.subscribe(listener)
5. SelectorsEach component reads its piece.
useSelector((state) => state.cart.items.length)
AsyncA thunk waits for the server, then dispatches actions.
createAsyncThunk("cart/load", async () => ...)Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Run Trace.jsx and click twice. Find the line where the state actually changes.”
“Add a builder.addCase(loadCart.pending, ...) that sets a loading flag. What new lines appear in the log after dispatch(loadCart())?”
“Change the logger so it does not call next(action) for cart/added. What happens to the screen?”
“Write a paper plan for "dark mode toggle": state shape, actions, selectors, components. Then check Lesson 8 - does it even belong in Redux?”
What usually goes wrong
The UI only describes what happened. The reducers decide the new state.
✗ onClick={() => { state.cart.items.push("Docker"); }}✓ onClick={() => dispatch(added("Docker"))}Reducers must be pure and synchronous. Put async work in a thunk and dispatch the result.
✗ added(state) { fetch("/api/cart"); ... }✓ createAsyncThunk("cart/load", async () => fetchCart())Without a plan, actions get named after buttons and state gets duplicated. Fill in the paper plan first.
Every reducer is called for every action. A reducer must return its state unchanged for actions it does not handle - never undefined.
✗ function userReducer(state, action) { if (action.type === "user/renamed") return {...}; }✓ function userReducer(state = initial, action) { ...; return state; }Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Put these steps in the right order: selector runs, reducers calculate the new state, user clicks, store notifies subscribers, middleware sees the action, component re-renders, dispatch(action).
Show hintHide hint
Follow the arrows in the first diagram.
Show solutionHide solution
1. user clicks
2. dispatch(action)
3. middleware sees the action
4. reducers calculate the new state
5. store notifies subscribers
6. selector runs
7. component re-rendersWhy was the user reducer called when the action was cart/added? What must it return?
Show hintHide hint
Who decides which actions a reducer cares about?
Show solutionHide solution
The store calls every reducer for every action; each reducer decides
whether the action concerns it. The user reducer does not handle
cart/added, so it must return its current state unchanged.Write a paper plan for a "theme toggle" feature: state shape, actions, selectors and components.
Show hintHide hint
Two values, one action, one selector - and ask whether it belongs in Redux at all.
Show solutionHide solution
State shape: theme: { mode: "day" | "night" }
Action: themeToggled() day <-> night
Selector: selectThemeMode(state) -> "day" | "night"
Components: ThemeButton dispatches themeToggled; App reads the mode
But (Lesson 8): the theme changes rarely and needs no tracing -
React Context or a simple useState at the top is usually enough.Plan it, then build it: notifications
A notification bell shows the number of unread notifications; a dropdown lists them. Users can read one, or mark all as read. Write the paper plan first, then the slice and selectors, and test them in Node with a few dispatches.
- A paper plan: state shape, three actions, two selectors, components
- A slice implementing the three actions
- selectUnreadCount for the bell, selectNotifications for the dropdown
- A short script that dispatches and prints the unread count after each step
Show one solutionHide solution
import { configureStore, createSlice } from "@reduxjs/toolkit";
// Paper plan -> code
// state: { list: [{ id, text, read }] }
// actions: notificationReceived({ id, text }), notificationRead(id), allRead()
// selectors: selectUnreadCount (bell badge), selectNotifications (dropdown)
const notificationsSlice = createSlice({
name: "notifications",
initialState: { list: [] },
reducers: {
notificationReceived(state, action) {
state.list.unshift({ ...action.payload, read: false });
},
notificationRead(state, action) {
const item = state.list.find((n) => n.id === action.payload);
if (item) item.read = true;
},
allRead(state) {
state.list.forEach((n) => { n.read = true; });
},
},
});
const { notificationReceived, notificationRead, allRead } = notificationsSlice.actions;
const selectUnreadCount = (state) => state.notifications.list.filter((n) => !n.read).length;
const selectNotifications = (state) => state.notifications.list;
const store = configureStore({ reducer: { notifications: notificationsSlice.reducer } });
store.dispatch(notificationReceived({ id: 1, text: "New lesson: Redux 9" }));
store.dispatch(notificationReceived({ id: 2, text: "Someone replied to you" }));
console.log("unread:", selectUnreadCount(store.getState()));
store.dispatch(notificationRead(2));
console.log("unread after reading #2:", selectUnreadCount(store.getState()));
store.dispatch(allRead());
console.log("unread after allRead:", selectUnreadCount(store.getState()));
console.log("dropdown:", selectNotifications(store.getState()).map((n) => n.text));
// unread: 2
// unread after reading #2: 1
// unread after allRead: 0
// dropdown: [ 'Someone replied to you', 'New lesson: Redux 9' ]Key points
- One store holds the shared state; data moves in one direction around a loop.
- UI -> dispatch(action) -> middleware -> reducers -> store -> notify -> selectors -> UI.
- The UI never writes state; it describes what happened with an action.
- Every reducer is called for every action; reducers that do not care return their state unchanged.
- The state is one object with one key per slice; each slice owns only its key.
- Async work lives in thunks, which dispatch plain pending and fulfilled actions.
- Plan a feature on paper - shape, actions, selectors, components - before writing code.
Quick check before you move on
Interview questions
Describe the Redux data flow.
A component dispatches an action describing what happened. It passes through middleware, then the store calls the root reducer, which delegates to each slice reducer to produce the new state. The store saves it and notifies subscribers; react-redux runs each component’s selector and re-renders those whose selected value changed.
Why does Redux use unidirectional data flow?
Because every change follows the same path - dispatch, reducers, new state - so state changes are predictable, traceable and replayable. Views never mutate state, and reducers never touch views.
Where do side effects like API calls belong in Redux?
Outside reducers - in thunks, listener middleware or RTK Query. They perform the side effect and dispatch plain actions describing the results, which reducers then handle purely.
How would you structure a Redux codebase?
By feature: one slice file per feature containing its state, reducers, actions and selectors, next to its components, plus one store file that combines the slices.
Quiz
- 1.
In the click trace, what came right after "middleware sees cart/added"?
- 2.
Why did the selector run twice after one click?
- 3.
After cart/load/pending, why did the component not re-render?
- 4.
What did store.getState() look like after the load?
- 5.
In the "Save lesson" plan, what happens when the same lesson is saved twice?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned