/** * Stores are just seed + reduce function. * Notice they are plain objects and don't own the state. */ const countUpStore = { seed: { counter: 0 }, reduce(state, action) { switch (action.type) { case 'increment': return { ...state, counter: state.counter + 1 }; case 'decrement': return { ...state, counter: state.counter - 1 }; default: return state; } } }; const countDownStore = { seed: { counter: 10 }, reduce(state, action) { // Never mind that I'm doing the opposite of what action says: I'm just // showing that stores may handle actions differently. switch (action.type) { case 'increment': return { ...state, counter: state.counter - 1 }; case 'decrement': return { ...state, counter: state.counter + 1 }; default: return state; } } }; /** * Dispatcher receives an array of stores and manages a global state atom, * giving each store a slice of that atom using store index as an ID. * * It seeds the atom with the initial values and returns a dispatch function * that, when called with an action, will gather the new reduced state and * update the cursor with it. */ function createDispatcher(cursor, stores) { // Create the seed atom const seedAtom = stores.map(s => s.seed); cursor.set(seedAtom); return function dispatch(action) { // Create an atom with the next state of stores const prevAtom = cursor.get(); const nextAtom = stores.map((store, id) => store.reduce(prevAtom[id], action) ); cursor.set(nextAtom); } } /** * Creates a cursor that holds the value for the state atom. */ function createCursor() { let atom = null; return { get: () => atom, set: (nextAtom) => atom = nextAtom }; } /** * A cursor middleware that lets consumer observe() mutations to individual stores. */ function makeObservable(cursor) { const observers = []; /** * Observes a store by its ID. * Returns a real observable! */ function observe(id) { if (!observers[id]) { observers[id] = []; } function subscribe(observer) { // Immediately fire the current value (Zalgo!) const atom = cursor.get(); observer.onNext(atom[id]); // Subscribe const storeObservers = observers[id]; storeObservers.push(observer); function dispose() { // Unsubscribe const index = storeObservers.indexOf(observer); if (index > -1) { storeObservers.splice(index, 1); } } return { dispose }; } return { subscribe }; } const wrapper = { get() { return cursor.get(); }, set(nextAtom) { const prevAtom = cursor.get(); cursor.set(nextAtom); // Walk through each store's slice for (let id = 0; id < nextAtom.length; id++) { if (!observers[id] || !observers[id].length) { continue; } // Notify the observers if state is referentially unequal if (!prevAtom || prevAtom[id] !== nextAtom[id]) { observers[id].forEach(o => o.onNext(nextAtom[id]) ); } } } }; return { observe, cursor: wrapper }; } it('whatever', () => { let cursor = createCursor(); let observe; // Wrap cursor into the observation middleware: ({ cursor, observe } = makeObservable(cursor)); // Pass stores to dispatcher const dispatch = createDispatcher(cursor, [countDownStore, countUpStore]); // We can now subscribe to store's individual updates without // any involvement from the stores themselves: const subscription = observe(1/* index in store array */).subscribe({ onNext(countUpState) { console.log('countup store state', countUpState); } }); // Dispatch actions: dispatch({ type: 'increment' }); dispatch({ type: 'increment' }); dispatch({ type: 'increment' }); dispatch({ type: 'decrement' }); // Unsubscription: subscription.dispose(); dispatch({ type: 'decrement' }); // Silent // The *really* interesting part is left as an exercise to the reader: // // // let cursor = createCursor(); // let observe, peekAtPast, lock, unlock; // ({ cursor, peekAtPast } = makePeekable(cursor)); // NEW! records values // ({ cursor, lock, unlock } = makeLockable(cursor)); // NEW! ignores current atom and forces a constant // ({ cursor, observe } = makeObservable(cursor)); // observe at the end of the chain // // // Some boring stuff: // // // const dispatch = createDispatcher(cursor, [countDownStore, countUpStore]); // const subscription = observe(1/* index in store array */).subscribe({ // onNext(countUpState) { // console.log('countup store state', countUpState); // } // }); // dispatch({ type: 'increment' }); // dispatch({ type: 'increment' }); // dispatch({ type: 'increment' }); // dispatch({ type: 'increment' }); // // // ... now comes the interesting part. // // // const pastAtom = peekAtPast(2); // NEW! reaches back in time // lock(pastAtom); // NEW! forces change handlers to always receive pastAtom instead of current atom // ... // unlock(); // NEW! switches to emit the current atom again // // // Do you see? Because makeObservable() is last in chain, it will receive // the values from makeLockable(). We can make a time travel interface on top of it, // and components will receive past values as you drag a slider, but stores have // *zero* knowledge of it and need no special time travelling logic. })