Navigate
Search topics, hooks, and fundamentals
What if ground control had a single Flight Commander processing every command through one unified protocol?
π Theme: Space Launch Mission Control
Your component is a mission control center. Multiple systems (fuel, oxygen, comms) need coordinated updates. Without useReducer, each operator uses their own radio β chaos. The Flight Commander (reducer) processes every command through one protocol: receive an action, return the new state. Predictable, testable, and one source of truth.
5 systems, 5 operators, 5 separate radios β and a reset that requires calling every single station.
Changed 0 systems β each has its own operator!
Same ship, but all commands go through one Flight Computer. Dispatch a command, and the reducer computes the new ship state.
All commands flow through dispatch() β the Flight Computer decides!
const initialShipState = {
fuel: 100, altitude: 0, shields: 50,
commsChannel: "Alpha", missionPhase: "Pre-Launch",
};
function shipReducer(state, action) {
switch (action.type) {
case "SET_FUEL": return { ...state, fuel: action.value };
case "SET_ALTITUDE": return { ...state, altitude: action.value };
case "SET_SHIELDS": return { ...state, shields: action.value };
case "SET_COMMS": return { ...state, commsChannel: action.channel };
case "TOGGLE_PHASE": return { ...state, missionPhase:
state.missionPhase === "Pre-Launch" ? "In-Flight" : "Pre-Launch" };
case "RESET": return initialShipState; // One command resets everything!
}
}
const [state, dispatch] = useReducer(shipReducer, initialShipState);useReducer is like a Flight Commander β all state changes are commands routed through one Flight Computer.
useReducer shines with complex state β mission checklists, cargo manifests, and launch sequences. Run these operations.
1 of 3 objectives completed
type ChecklistAction =
| { type: "LOG_OBJECTIVE"; text: string }
| { type: "COMPLETE_OBJECTIVE"; id: number }
| { type: "SCRUB_OBJECTIVE"; id: number }
| { type: "ARCHIVE_COMPLETED" };
function checklistReducer(state, action) {
switch (action.type) {
case "LOG_OBJECTIVE":
return [...state, { id: nextId++, text: action.text, completed: false }];
case "COMPLETE_OBJECTIVE":
return state.map(obj =>
obj.id === action.id ? { ...obj, completed: !obj.completed } : obj);
case "SCRUB_OBJECTIVE":
return state.filter(obj => obj.id !== action.id);
case "ARCHIVE_COMPLETED":
return state.filter(obj => !obj.completed);
}
}Supply Depot
Fuel Cell
50 kg
Shield Module
120 kg
Ration Pack
5 kg
Cargo Manifest
Bay is empty. Load supplies from the depot.
type CargoAction =
| { type: "LOAD_CARGO"; item: { id, name, weight } }
| { type: "JETTISON"; id: string }
| { type: "ADJUST_LOAD"; id: string; delta: number }
| { type: "PURGE_BAY" };
function cargoReducer(state, action) {
switch (action.type) {
case "LOAD_CARGO": {
const existing = state.items.find(i => i.id === action.item.id);
if (existing) {
return { items: state.items.map(i =>
i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i) };
}
return { items: [...state.items, { ...action.item, qty: 1 }] };
}
case "JETTISON":
return { items: state.items.filter(i => i.id !== action.id) };
case "ADJUST_LOAD":
return { items: state.items
.map(i => i.id === action.id
? { ...i, qty: Math.max(0, i.qty + action.delta) } : i)
.filter(i => i.qty > 0) };
case "PURGE_BAY":
return { items: [] };
}
}The reducer enforces valid transitions only. No skipping from IDLE to ORBIT!
type MissionPhase = "IDLE" | "PRE_CHECK" | "COUNTDOWN" | "LAUNCH" | "ORBIT";
function launchReducer(state: MissionPhase, action) {
switch (action.type) {
case "ADVANCE":
if (state === "IDLE") return "PRE_CHECK";
if (state === "PRE_CHECK") return "COUNTDOWN";
if (state === "COUNTDOWN") return "LAUNCH";
if (state === "LAUNCH") return "ORBIT";
return state; // ORBIT is terminal
case "ABORT":
return "IDLE"; // Any phase can abort
}
}These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
function reducer(state, action) {
switch (action.type) {
case "toggle_fuel":
state.fuel = !state.fuel; // mutating existing state!
return state; // same reference β React won't re-render
default:
return state;
}
}function reducer(state, action) {
switch (action.type) {
case "set_fuel":
return { fuel: action.value }; // oxygen, comms, etc. are lost!
default:
return state;
}
}function reducer(state, action) {
switch (action.type) {
case "launch":
fetch("/api/launch", { method: "POST" }); // side effect!
console.log("Launching..."); // side effect!
return { ...state, launched: true };
default:
return state;
}
}