MichalZalecki/connect-rxjs-to-react

Scope and initial state per reducer (combineReducers)

Closed this issue · 9 comments

I tried to make an app similar to Redux and moved the initialization state in reducer

file: createState.js

import Rx from "rxjs"

function createState(reducer$, initialState = {}) {
  return reducer$
    .scan((state, reducer) => reducer(state), initialState)
    .publishReplay(1)
    .refCount()
}

export default createState

file: CounterReducer.js

import Rx from 'rxjs'

const initialState = {
  counter: 3,
}

const CounterReducer$ = Rx.Observable.of(_ => initialState).merge(
  CounterActions.increment$.map((n = 1) =>
    state => ({ ...state, counter: state.counter+n })),

  CounterActions.decrement$.map((n = 1) =>
    state => ({ ...state, counter: state.counter-n }))
)
//naming reducer
.map(reducer => ['one', reducer])

export default CounterReducer$

file: state.js

import Rx from "rxjs";
import createState from "app/rx-state/createState";
import CounterReducer$ from "app/reducers/CounterReducer";

const reducer$ = Rx.Observable.merge(
  CounterReducer$,
)
//return a function which expects store state
.map(([name, reducer]) => (store) => ({...store, [name]: reducer(store[name] || {})}))

let initialState;

export default createState(reducer$, initialState);

what do you say about this approach?

@b2whats I'd move this logic from state.js to createState.js and still keep global initial state as an observable. "Naming" reducer in state.js allows to reusing the same reducer. I like the idea of scoped reducer, thanks! I'll link to that issue in the blogpost.

I came up with this:

// state.js
import Rx from "rxjs";
import createState from "app/rx-state/createState";
import CounterReducer$ from "app/reducers/CounterReducer";

// "counter" and "otherCounter" are like mounting points of the reducer
// can be easily turn into combineReducers
const reducer$ = Rx.Observable.merge(
  CounterReducer$.map(reducer => ["counter", reducer]),
  CounterReducer$.map(reducer => ["otherCounter", reducer]),
);

export default createState(reducer$);
// createState.js

import Rx from "rxjs";

function createState(reducer$, initialState$ = Rx.Observable.of({})) {
  return initialState$
    .merge(reducer$)
    .scan((state, [scope, reducer]) => ({ ...state, [scope]: reducer(state[scope]) }))
    .publishReplay(1)
    .refCount();
}

export default createState;
// CounterReducer.js

import Rx from "rxjs";
import CounterActions from "app/actions/CounterActions";

const CounterReducer$ = Rx.Observable.merge(
  CounterActions.increment$.map((n = 1) => counterState => counterState + n),

  CounterActions.decrement$.map((n = 1) => counterState => counterState - n),
)
.startWith(() => 10); // function which returns initial state (optional)

export default CounterReducer$;

Then initial state works wrong. In first render we have empty state

http://codepen.io/anon/pen/ZWdEKB?editors=0010

It's not wrong in my opinion. It's just an initial state which in this case is an empty object and that's what will be emitted.

but the component in the first renderer expects the default state which we have described in reduce

Valid concerns, but no worries. Componens subscribe after state is created with all "default" reducers. Copy&pase the code into the project and you will see.

your example passes the single value, my = {}
in fitst render props = {}

component not subscribe after state is created with all "default".
file: connec.js

      componentWillMount() {
        this.subscription = state$.map(selector).subscribe(::this.setState);
      }

first render in your example with my implementation
Rx.Observable.of({}) // props = {}

export default connect(state$, state => ( {
  counter: state.one.counter, // state = {} state.one = undefined state.one.counter = Error
  ...CounterActions,
}))(Counter);

second render
first reducer - .startWith(() => {counter: 1}); // props = {one: {counter : 1}}

Copy&pase the code into the project and you will see.

console.count("render");

image

Please, for further discussion provide an example where it behaves differently.

one moment, will do the fork and show you what I mean