From redux store to Potions declarative store

How Potions store get rid of redux store imperative behavior
profile photo
Claire Molinier
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$
    • javascript
      reducer$.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.
javascript
const 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
javascript
const 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
javascript
const 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
Redux
Related posts
post image
Core concepts of Potions Reactive framework
post image
Reactive framework
Choosing RxJS
Observable subscription is the glue missing to Javascript
post image
Reactive frameworks : state of the art
Powered by Notaku