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);
Output
["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

Write 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
Try it in the editor →

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.

Learn JavaScript Intermediate for free

20 short lessons with real code and instant checks.

See the whole course →