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 });
  }
}