Chapter 3 of 4
A Worked Example
A complete todo reducer, from actions to component.
const initialState = { items: [], filter: "all" };
function todoReducer(state, action) {
switch (action.type) {
case "added":
return {
...state,
items: [...state.items, { id: action.id, text: action.text, done: false }],
};
case "toggled":
return {
...state,
items: state.items.map((item) =>
item.id === action.id ? { ...item, done: !item.done } : item
),
};
case "deleted":
return {
...state,
items: state.items.filter((item) => item.id !== action.id),
};
case "cleared_completed":
return { ...state, items: state.items.filter((item) => !item.done) };
case "filter_changed":
return { ...state, filter: action.filter };
default:
throw new Error("Unknown action: " + action.type);
}
}function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialState);
// Derived, not stored - it can never fall out of sync
const visible = state.items.filter((item) =>
state.filter === "all" ? true :
state.filter === "done" ? item.done : !item.done
);
return (
<>
<NewTodoForm
onAdd={(text) =>
dispatch({ type: "added", id: crypto.randomUUID(), text })
}
/>
<ul>
{visible.map((item) => (
<TodoRow
key={item.id}
item={item}
onToggle={() => dispatch({ type: "toggled", id: item.id })}
onDelete={() => dispatch({ type: "deleted", id: item.id })}
/>
))}
</ul>
<FilterBar
value={state.filter}
onChange={(filter) => dispatch({ type: "filter_changed", filter })}
/>
</>
);
}Where async work goes
Reducers stay pure, so requests happen in event handlers or effects, and their results are dispatched as actions.
async function handleSave(item) {
dispatch({ type: "save_started" });
try {
const saved = await api.save(item);
dispatch({ type: "save_succeeded", item: saved });
} catch (error) {
dispatch({ type: "save_failed", message: error.message });
}
}