0

How can I share the startWith(false) state between the 3 streams? I tried using withLatestFrom() but got some weird errors for the value.

const Home = componentFromStream(prop$ => {
  const { handler: toggleHandler, stream: toggle$ } = createEventHandler();
  const { handler: showHandler, stream: show$ } = createEventHandler();
  const { handler: hideHandler, stream: hide$ } = createEventHandler();

  const modal$ = merge(
    toggle$.pipe(
      startWith(false),
      map(() => prev => !prev),
      scan((state, changeState: any) => changeState(state))
    ),
    show$.pipe(
      startWith(false),
      map(() => prev => true),
      scan((state, changeState: any) => changeState(state))
    ),
    hide$.pipe(
      startWith(false),
      map(() => prev => false),
      scan((state, changeState: any) => changeState(state))
    )
  );

  return combineLatest(prop$, modal$).pipe(
    map(([props, modal]) => {
      console.log(modal);
      return (
        <div>
          <button onClick={toggleHandler}>Toggle</button>
          <button onClick={showHandler}>Show</button>
          <button onClick={hideHandler}>Hide</button>
          <h1>{modal ? 'Visible' : 'Hidden'}</h1>
        </div>
      );
    })
  );
});

In the example, the toggle doesn't respect the current value of show or hide, but only of its own latest value.

https://stackblitz.com/edit/react-rxjs6-recompose

Webber
  • 941
  • 11
  • 26
  • `merge` subscribes to all source Observables at the same time so all three sources will emit `startWith(false)` immediately which is not what you want I guess? – martin Jan 20 '19 at 12:45

3 Answers3

2

In order to do this, you will need to manage state a bit differently, something similar to what people do in redux. Take a look at example:

const { of, merge, fromEvent } = rxjs; // = require("rxjs")
const { map, scan } = rxjs.operators; // = require("rxjs/operators")

const toggle$ = fromEvent(document.getElementById('toggle'), 'click');
const show$ = fromEvent(document.getElementById('show'), 'click');
const hide$ = fromEvent(document.getElementById('hide'), 'click');

const reduce = (state, change) => change(state);

const initialState = false;
const state$ = merge(
  of(e => e),
  toggle$.pipe(map(e => state => !state)),
  show$.pipe(map(e => state => true)),
  hide$.pipe(map(e => state => false)),
).pipe(
  scan(reduce, initialState),
);

state$.subscribe(e => console.log('state: ', e));
<script src="https://unpkg.com/rxjs@6.2.2/bundles/rxjs.umd.min.js"></script>


<button id="toggle">Toggle</button>
<button id="show">Show</button>
<button id="hide">Hide</button>

To better understand how it works, take a look at Creating applications article from rxjs documentation

Oles Savluk
  • 4,315
  • 1
  • 26
  • 40
  • 1
    This is imho not correct solution. First it doesn't emit anything until one of observables emits. Second and most importantly toggle will work only with last state of toggle button. Therefore it doesn't toggle after you change the value with show/hide button. – Martin Nuc Jan 21 '19 at 16:59
  • @MartinNuc the toggle seems to consider the state of hide/show when I tested this out – Webber Jan 21 '19 at 23:12
  • I think semantically using `startWith()` to set the value is more intuitive than setting the initial value in `scan()` – Webber Jan 21 '19 at 23:23
0

First having startWith on all those observables means they will all emit at the beginning false. Which is like you pushed all those buttons at once.

I think you should try to achieve different behaviour. Using startWith you want to set the initial state of the modal property, right? Therefore it should come after merging those stream together.

To toggle the value you need two things:

  1. place where to store the state (usually accumulator of scan)
  2. differentiate those button presses. You have three buttons therefore you need three values.

This is my approach:

 const modal$ = merge(
    show$.pipe(mapTo(true)),
    hide$.pipe(mapTo(false)),
    toggle$.pipe(mapTo(null))
  ).pipe(
    startWith(false),
    scan((acc, curr) => {
      if (curr === null) {
        return !acc;
      } else {
        return curr;
      }
    })
  );

Show button always emits true, hide button emits false and toggle button emits null. I merge them together and we want to start with false. Next there is scan which holds the state in accumulator.

When null comes it returns negated state. When true or false comes it returns it - that way it sets the new state regardless the previous value.

Martin Nuc
  • 5,604
  • 2
  • 42
  • 48
  • Thanks for taking the time to answer this! The answer works, but moving the toggle logic into scan seems a bit more difficult to read, the other approach seems more intuitive. – Webber Jan 21 '19 at 23:19
  • 1
    No problem. Passing a function is very elegant solution which didnt come up to my mind. One always learns something new :-) – Martin Nuc Jan 21 '19 at 23:24
0

I took everyone's approach & came up with this. It keeps the toggle() logic inside the map function, & uses startWith() to set the value.

  const modal$ = merge(
    toggle$.pipe(
      map(() => prev => !prev),
    ),
    show$.pipe(
      map(() => prev => true),
    ),
    hide$.pipe(
      map(() => prev => false),
    )
  ).pipe(
    startWith(false),
    scan((state, change) => change(state)),
  );
Webber
  • 941
  • 11
  • 26