Capstone project
Capstone: a state store
Lesson 20 of 20 · 28 min · Module 6: Asynchronous programs
Many web apps keep their data in a central store: the state changes only through described actions, and every part of the page that cares is notified. Build a small version of that pattern, used by libraries such as Redux, and a to-do reducer to run in it.
Start this lesson →Free. No installation: you write and run the code in your browser, and it is checked automatically.
By the end, you’ll be able to…
- Keep application state in one place
- Write pure reducers that never mutate
- Notify subscribers and allow unsubscribing
The idea
A reducer is a pure function (state, action) → newState that never changes its input. The store holds the current state in a closure, applies actions with dispatch, and calls every subscribed listener after each change.
const counter = (state = 0, action) => action.type === "inc" ? state + 1 : state;
let state = counter(undefined, { type: "init" });
state = counter(state, { type: "inc" });
console.log(state);
01Actions describe what happened
{ type: "toggle", id: 1 } is plain data. Because every change goes through dispatch, you can log, replay or undo actions, and you always know why the state changed.
02Reducers are pure
todos only computes a new array from its inputs. With no hidden changes, the same state and action always produce the same result, which makes reducers easy to test.
const add = (state, text) => [...state, text];
const before = ["a"];
const after = add(before, "b");
console.log(before, after);["a"] ["a","b"]03Same state means no update
Returning the exact same array when nothing changed lets the store skip notifying listeners. In real apps this avoids redrawing parts of the page for no reason.
Your project brief
createStore(reducer, initialState) returning { getState, dispatch, subscribe }:•
dispatch(action) throws a TypeError unless action is an object with a string type; otherwise it sets the state to reducer(state, action), calls every listener with the new state only if it changed (compare with !==), and returns the action;•
subscribe(listener) returns an unsubscribe function.Write
todos(state = [], action) handling "add" ({ text } → append { id, text, done: false } with id one more than the largest id, starting at 1; ignore blank text), "toggle" ({ id }), "remove" ({ id }) and "clear-done". It must never change its input and must return the same state for unknown actions or ids.What the checks look for
- The store applies actions and returns them
- Listeners run after real changes only and can unsubscribe
- Invalid actions throw a TypeError
- todos adds, toggles, removes and clears
- todos never changes its input and keeps the same state when nothing changes
- New ids continue from the largest existing id
Take this with you
A store is a closure, a reducer, and a set of listeners working together.
Go a little further
Add an undo action by keeping a history of previous states inside createStore.