MichalZalecki / connect-rxjs-to-react

Connect rxjs to React component in Redux style... but without dispatch and constants.
187 stars 27 forks source link

Scope and initial state per reducer (combineReducers) #3

Closed b2whats closed 6 years ago

b2whats commented 8 years ago

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?

MichalZalecki commented 8 years ago

@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$;
b2whats commented 8 years ago

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

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

MichalZalecki commented 8 years ago

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.

b2whats commented 8 years ago

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

MichalZalecki commented 8 years ago

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.

b2whats commented 8 years ago

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}}

MichalZalecki commented 8 years ago

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

https://github.com/MichalZalecki/connect-rxjs-to-react/blob/3faf7e5c9737a7f72c83f990276c314377422216/src/components/Counter.js#L15

image

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

b2whats commented 8 years ago

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

b2whats commented 8 years ago

https://github.com/MichalZalecki/connect-rxjs-to-react/pull/4