State plus an action produces next state
A reducer is a pure function receiving current state and an action record, then returning the next state. An action says what happened, such as loading, loaded, failed, or toggle. It does not perform the request itself.
The switch statement compares one value with named cases. Each return exits the function, so there is no fall-through here. A default case rejects unknown actions: that helps catch a typo rather than quietly producing a misleading UI. Another system might deliberately ignore unknown actions; define your policy.
The reading queue’s state contains status, results, saved, and error. Starting a search clears old results and errors but keeps saved books. A successful empty list is ready. Failure keeps saved books and supplies feedback. Cancelling returns to idle. These are product decisions, and tests should express them.
Do not store values you can reliably derive, such as savedCount, alongside saved unless there is a reason. Duplicated state creates more transitions that must stay synchronized. Keep HTTP, rendering, and storage outside the reducer so its tests need no browser.
Predict and trace
Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.
function reduce(state, action) {
switch (action.type) {
case "loading":
return { ...state, status: "loading", results: [], error: "" };
case "loaded":
return { ...state, status: "ready", results: action.books, error: "" };
default:
throw new Error("Unknown action");
}
}
const before = {
status: "idle",
results: [],
saved: [{ id: "js" }],
error: ""
};
const after = reduce(before, { type: "loaded", books: [] });
console.log(before.status);
console.log(after.status);
console.log(after.saved.length);Check your prediction
idle
ready
1The empty successful result changes status without erasing saved books or mutating the previous state.
Actions describe events; reducers apply rules
A reducer receives the previous state and an action, then computes the next state. It should not start a request, write storage or render a page inside that calculation. Keeping side effects outside makes a transition reproducible with the same arguments.
A transition should preserve unrelated state. Loading new results must not silently clear the saved list unless that is the stated policy. Test both the changed field and the fields that should survive; a correct status alone is insufficient.
function reduce(state, action) {
if (action.type === "save") {
return { ...state, saved: [...state.saved, action.book] };
}
return state;
}
const before = { query: "loops", saved: [] };
const after = reduce(before, { type: "save", book: { id: "a" } });
console.log(before.saved.length, after.saved.length);
console.log(after.query);Check your prediction
0 1
loops The new saved array and outer state preserve the old snapshot. Query is retained through the spread. None of this requires I/O in the reducer.
function reduce(state, action) {
return action.type === "reset" ? { count: 0 } : state;
}
const before = { count: 2 };
console.log(reduce(before, { type: "unknown" }) === before);
console.log(reduce(before, { type: "reset" }).count);Check your prediction
true
0 This toy chooses to ignore unknown actions. The earlier full reducer chooses to throw; both require an explicit contract. Do not accidentally change the policy while refactoring a switch.
Try the rule on new inputs
For each real action, list changed and preserved fields. Test an unknown action according to the actual reducer contract. Keep async scheduling tests in the controller layer, where the side effects belong.
Build it yourself
Reducers & Actions: practise
Write a failed transition returning a new state with status error, no results, and the supplied message, while preserving saved. Print status, message, and saved length.
const state = {
status: "ready",
results: [{ id: "web" }],
saved: [{ id: "js" }]
};Hint 1: the idea
Start from the old fields, then replace the fields the action owns.
Hint 2: a first step
Spread state first; later properties override its old status and results.
Show one solution
function fail(state, message) {
return { ...state, status: "error", results: [], error: message };
}
const state = {
status: "ready",
results: [{ id: "web" }],
saved: [{ id: "js" }]
};
const next = fail(state, "Try again");
console.log(next.status);
console.log(next.error);
console.log(next.saved.length);error
Try again
1Check the original results length is still one. Then specify a cancelled transition that clears errors but preserves saved books.
Test a changed requirement
Check the original results length is still one. Then specify a cancelled transition that clears errors but preserves saved books.
Explain before moving on
What is an action, and who performs the effect that leads to it? Why does loading preserve saved?
Completion is saved in this browser only. It does not measure mastery.