Skip to the code
cf.

Redux-style store

Write createStore(reducer, initialState), the core of Redux. It returns a store object with three methods.

getState() returns the current state.

dispatch(action) sets the state to reducer(state, action), then calls every subscriber with no arguments, and returns action.

subscribe(listener) adds a subscriber and returns a function that removes it.

When the store is created, it dispatches { type: "@@INIT" } once, so a reducer's default state applies when initialState is undefined. Nobody can have subscribed yet, so that dispatch only sets the state.

Subscribing or unsubscribing during a dispatch takes effect from the next dispatch. A reducer that calls dispatch gets an Error thrown at it, and the store keeps working afterwards.

const counter = (state = 0, action) => (action.type === "increment" ? state + 1 : state);
const store = createStore(counter);
store.getState(); // 0
 
const unsubscribe = store.subscribe(() => console.log("now", store.getState()));
store.dispatch({ type: "increment" }); // logs "now 1"
unsubscribe();
store.dispatch({ type: "increment" }); // logs nothing; the state is 2

What the tests check

  1. the reducer's default state applies when there's no initial state
    const counter = (state = 0, action) => (action.type === "inc" ? state + 1 : state);
    const store = createStore(counter);
    expect(store.getState()).toBe(0);
    
  2. starts from initialState when it's given
    const store = createStore((state = 0) => state, 5);
    expect(store.getState()).toBe(5);
    
  3. dispatches { type: "@@INIT" } once when it's created
    const reducer = spy((state = 0) => state);
    createStore(reducer);
    expect(reducer).toHaveBeenCalledTimes(1);
    expect(reducer).toHaveBeenCalledWith(undefined, { type: "@@INIT" });
    
  4. dispatch runs the reducer on the current state and stores the result
    const reducer = spy((state = 0, action) => (action.type === "add" ? state + action.by : state));
    const store = createStore(reducer);
    store.dispatch({ type: "add", by: 2 });
    store.dispatch({ type: "add", by: 3 });
    expect(reducer).toHaveBeenLastCalledWith(2, { type: "add", by: 3 });
    expect(store.getState()).toBe(5);
    
  5. dispatch returns the action
    const store = createStore((state = 0) => state);
    const action = { type: "x" };
    expect(store.dispatch(action)).toBe(action);
    
  6. calls subscribers after each dispatch until they unsubscribe
    const store = createStore((state = 0, action) => state + (action.by ?? 0));
    const seen = [];
    const unsubscribe = store.subscribe(() => seen.push(store.getState()));
    store.dispatch({ type: "add", by: 2 });
    unsubscribe();
    store.dispatch({ type: "add", by: 3 });
    expect(seen).toEqual([2]);
    expect(store.getState()).toBe(5);
    
  7. a subscriber added during a dispatch waits for the next one
    const store = createStore((state = 0) => state + 1);
    const late = spy();
    store.subscribe(() => store.subscribe(late));
    store.dispatch({ type: "x" });
    expect(late).not.toHaveBeenCalled();
    store.dispatch({ type: "x" });
    expect(late).toHaveBeenCalledTimes(1);
    
  8. unsubscribing during a dispatch doesn't skip anyone in it
    const store = createStore((state = 0) => state + 1);
    const log = [];
    let unsubscribeB;
    store.subscribe(() => {
      log.push("a");
      unsubscribeB();
    });
    unsubscribeB = store.subscribe(() => log.push("b"));
    store.subscribe(() => log.push("c"));
    store.dispatch({ type: "x" });
    store.dispatch({ type: "x" });
    expect(log).toEqual(["a", "b", "c", "a", "c"]);
    
  9. a reducer that dispatches gets an Error, and the store keeps working
    let store;
    const reducer = (state = 0, action) => {
      if (action.type === "nested") store.dispatch({ type: "inc" });
      return action.type === "inc" ? state + 1 : state;
    };
    store = createStore(reducer);
    expect(() => store.dispatch({ type: "nested" })).toThrow(Error);
    expect(store.getState()).toBe(0);
    store.dispatch({ type: "inc" });
    expect(store.getState()).toBe(1);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).