Here is a way to create a Redux store from scratch
At Potions, we think that
- dispatch isn’t right : it’s an imperative way to change the state and trigger subscribers (and we want to be as declarative as possible).
- why limit our store to one reducer ?? You’ll probably end up with one huge reducer with an amazing number of layers and switch cases.
- there should be a way to remove reducers
- last (and least) why is it the reducer’s duty to define the initial state of the store ?
This is our shot at those problems :
- a store holds a state$ that is an Rxjs BehaviorSubject.
- a reducer is now an operator that takes an observable and returns an observable.
- there can be more than one reducer and adding a reducer$ is equivalent to adding a new subscription to the state$
javascriptreducer$.subscribe(state$)
where reducer$ is an observable and often the result of a reducer function reducer(source$, state$)
It returns a subscription with an unsubscribe method
- dispatch disappears, it is simply a next in the source.
- subscribe disappears and is replaced by a getState$().subscribe() with getState$ returning the state$ as an observable. This way we can apply RxJS operators prior to subscribing.
This is the core of our changes and this becomes our new createStore function
javascript// Redux store with a BehaviorSubject state const createStore = (init = null) => { const state$ = new BehaviorSubject(init); const getState = () => state$.value; const getState$ = () => state$.asObservable(); const addReducer = reducer_function => source$ => { const reducer = map(action => reducer_function(state$.value, action)); return reducer(source$).subscribe(state$); }; return { getState, addReducer, subscribe }; };
Here is how you would implement a counter example, that sticks to Dan Abramov’s reducer function.
javascriptconst store = createStore(0); const action$ = new Subject(); const counter = (state, action) => { switch (action.type) { case "INCREMENT": return state + 1; case "DECREMENT": return state - 1; default: return state; } }; const counterReducerSubscription = store.addReducer(counter)(action$); action$.next({ type: "INCREMENT" }); console.log(store.getState());//logs 1 action$.next({ type: "INCREMENT" }); console.log(store.getState());//logs 2 counterReducerSubscription.unsubscribe(); action$.next({ type: "INCREMENT" }); console.log(store.getState());//logs 2 because we unsubscribed
We decided to extend the store object to a channelStore.
- our apps use a single global channel$ subject that acts as a hub for all key identified messages that are meant to change states in stores and that we want to track.
- we decided to have one reducer per message to avoid switch cases and to add and remove easily subscriptions.
We no longer talk about actions with a type but a message with a key.
javascript// ChannelStore const createChannelStore = (channel$, init = null) => { let store = createStore(init); let channelReducers = {}; const addChannelReducer = (messageKey, reducer) => { const source$ = channel$.pipe( filter(message => message.key === messageKey) ); channelReducers[messageKey] = store.addReducer(reducer)(source$); return channelReducers[messageKey]; }; const removeChannelReducer = messageKey => { channelReducers[messageKey]?.unsubscribe(); }; return { ...store, addChannelReducer, removeChannelReducer }; };
And the counter example becomes
javascriptconst channel$ = new Subject(); const counter = createChannelStore(channel$, 0); const INCREMENT = "increment" const DECREMENT = "decrement" counter.addChannelReducer(INCREMENT, (state, message) => state + 1); counter.addChannelReducer(DECREMENT, (state, message) => state - 1); channel$.next({ key: INCREMENT }); console.log(store.getState()); //logs 1 channel$.next({ key: INCREMENT }); console.log(store.getState()); //logs 2 store.removeChannelReducer(INCREMENT); channel$.next({ key: INCREMENT }); console.log(store.getState()); //logs 2 because we unsubscribed
We decided to extend the store object to a channelStore.
- our apps use a single global channel$ subject that acts as a hub for all key identified messages that are meant to change states in stores and that we want to track.
- we decided to have one reducer per message to avoid switch cases and to add and remove easily subscriptions.
We no longer talk about actions with a type but a message with a key.
javascript// ChannelStore const createChannelStore = (channel$, init = null) => { let store = createStore(init); let channelReducers = {}; const addChannelReducer = (messageKey, reducer) => { const source$ = channel$.pipe( filter(message => message.key === messageKey) ); channelReducers[messageKey] = store.addReducer(reducer)(source$); return channelReducers[messageKey]; }; const removeChannelReducer = messageKey => { channelReducers[messageKey]?.unsubscribe(); }; return { ...store, addChannelReducer, removeChannelReducer }; };
And the counter example becomes
Reduxjavascriptconst channel$ = new Subject(); const counter = createChannelStore(channel$, 0); const INCREMENT = "increment" const DECREMENT = "decrement" counter.addChannelReducer(INCREMENT, (state, message) => state + 1); counter.addChannelReducer(DECREMENT, (state, message) => state - 1); channel$.next({ key: INCREMENT }); console.log(store.getState()); //logs 1 channel$.next({ key: INCREMENT }); console.log(store.getState()); //logs 2 store.removeChannelReducer(INCREMENT); channel$.next({ key: INCREMENT }); console.log(store.getState()); //logs 2 because we unsubscribed