Lesson 69 of 80
A custom hook that lets its consumer inject their own reducer, so they can override how it transitions state without forking the hook's source.
16 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The state reducer pattern is a way of writing a custom hook or component so that the person using it can override how state transitions happen, not just what the current values are. Instead of hard-coding the logic that decides what an action does, the hook accepts a reducer from its caller and runs that instead of (or wrapped around) its own default.
This is a different problem than useReducer itself solves. useReducer is the mechanism — a hook that runs (state, action) => newState internally, inside one component. The state reducer pattern is one level up: it's a design choice made by whoever authors a reusable hook, to expose that internal reducer as a customization point for whoever consumes it. You already know how to write a reducer; this lesson is about handing that pen to someone else.
Start with an ordinary useToggle hook, built on useReducer like the earlier lesson:
function toggleReducer(state, action) {
switch (action.type) {
case "toggle":
return { on: !state.on, clicks: state.clicks + 1 };
case "reset":
return { on: false, clicks: 0 };
default:
throw new Error(`Unhandled action: ${action.type}`);
}
}
function useToggle({ reducer = toggleReducer } = {}) {
const [state, dispatch] = useReducer(reducer, { on: false, clicks: 0 });
const toggle = () => dispatch({ type: "toggle" });
const reset = () => dispatch({ type: "reset" });
return { ...state, toggle, reset };
}
The only thing that makes this the state reducer pattern is one line: reducer = toggleReducer is a parameter, not a hard-coded internal. Now a consumer who wants "stop toggling after 4 clicks" doesn't need to fork useToggle or ask its author to add a maxClicks prop — they write their own reducer that wraps the default:
function MaxFourToggle() {
function stateReducer(state, action) {
const next = toggleReducer(state, action); // run the real logic first
if (action.type === "toggle" && state.clicks >= 4) {
return { ...next, on: state.on }; // still counts the click, blocks the flip
}
return next;
}
const { on, clicks, toggle } = useToggle({ reducer: stateReducer });
return (
<button onClick={toggle}>
{on ? "on" : "off"} ({clicks} clicks)
</button>
);
}
stateReducer calls toggleReducer first, then adjusts the result. This is the pattern's key move: the consumer intercepts a transition instead of replacing the whole state machine, so useToggle's author never had to anticipate "someone will eventually want a click limit."
maxClicks prop instead, but that only covers the cases its author thought of. A state reducer covers cases nobody anticipated, because the consumer controls the actual transition logic, not a pre-built option.stateReducer does above) keeps one source of truth.stateReducer prop instead of a dozen individual behavior flags.useReducer gives one component a clean way to express "what happened" as a plain action and let one function decide the consequence. The state reducer pattern takes that same (state, action) => newState shape and turns it into a public extension point — the hook's author writes the default transition logic, but hands the actual decision to whoever calls the hook. It's the same idea from the reducer lesson, just moved from "internal implementation detail" to "part of the API."