Lately, I fell into a pretty tough RxJS beginner trap while playing around with Cycle.js.

Here is the situation I was in:

  • a parent component that creates a child component depending on a data stream;
  • a child component that outputs an action$ stream that is used in the parent component (depending on DOM events).

And the symptoms:

  • the child component renders perfectly;
  • I get the expected output if I subscribe to the action$ stream from inside the child component;
  • the action$ stream does not output anything from the parent component.

The reason for that weird behavior: the way cold observables work (and, of course, the fact that I didn't took the time to read and understand the RxJS doc ... :( )

NB: As I am writing this article, Cycle.js is being generalized to work with any stream library (see Cycle.js Diversity) and a stream library, specially designed for Cycle.js, is currently being built: xstream. As xtream will be hot-observable-only, this article won't be relevant any more :)

TLDR

Keep in mind that :

The situation

Ok let's go a little deeper into my newbie-trap.

Here is a simplified version of the situation I had.

The child component:

function UserContainer({DOM, user$}) {
    const logAction$ = DOM
        .select(".log")
        .events("click")
        .map(() => "log");

    //this prints an output each time the button is clicked
    logAction$.subscribe(/*log*/);

    return {
        //the DOM output of the component
        DOM: user$.map((user) => div([
            span(".user-name", user.name),
            button(".log", "Log")
        ])),

        //the 'log' stream that is used in the parent
        logAction$: logAction$
    };
}

The main function that creates the child component:

function main({DOM}) {
    const user$ = Observable.just({name: "felix"});

    //we map the user stream to an 'instance' of a UserContainer
    const userContainer$ = user$
      .map(user =>
          //from @cycle/isolate https://github.com/cyclejs/isolate
          isolate(UserContainer)({ DOM, user$: Observable.just(user) })
      )

    //keep a trace of the DOM evolution for the UserContainer component
    const userContainerDOM$ = userContainer$
      .map(container => container.DOM);

    //keep a link to the log action of the UserContainer
    const userContainerLogAction$ = userContainer$
      .flatMapLatest(container => container.logAction$);

    /*
     * /!\ here is the problem, this will never print anything
     * when the button is clicked !!
     */
    userContainerLogAction$.subscribe(/*log*/);

    return {
        DOM: userContainerDOM$
    };
}

If you want to play with it, here is a jsbin I created to reproduce the bug

To summary the symptoms I had:

  • I had no trouble receiving click events if I subscribe from inside the child component namely UserContainer;
  • for some reason I could receive any click events from the function where I build the UserContainer component.

The ridiculously simple solution

So – without any more suspense – here is the ridiculously simple solution:

const userContainer$ = user$
  .map(user =>
      //from @cycle/isolate https://github.com/cyclejs/isolate
      isolate(UserContainer)({ DOM, user$: Observable.just(user) })
  )
+ .shareReplay(1)

Explanation

Fixing a bug is a good thing, understanding it is a lot more valuable ;) So here is the explanation for that bug.

The main reason for that behavior is: the way cold observable work in RxJS. In fact a cold observable replays the whole observable sequence for each subscriber it has.

const values$ = Observable
    .just("test") //this is a cold observable
    .do(() => console.log("here I am"))
    .map(() =>
        /* /!\ just for the exemple.
         * always avoid doing non deterministic
         * calls in your app's code
         */
        Math.floor(Math.random() * 10)
    );

value$.subscribe((value) => console.log("first sub: " + value));
value$.subscribe((value) => console.log("second sub: " + value));

/* output:
 * Here I am
 * first sub: 7
 * Here I am
 * second sub: 5
 */

As you can the, the log Here I am is printed twice and the first subscriber doesn't get the same value as the second subscriber (resp 7 and 5).

I my case I have that piece of code that is building the UserContainer component:

const userContainer$ = user$
    .map(user =>
        isolate(UserContainer)({DOM, user$: Observable.just(user)})
    )

And two streams that originate from the userContainer$ stream:

const userContainerDOM$ = userContainer$
    .map(container => container.DOM);

const userContainerLogAction$ = userContainer$
    .flatMapLatest(container => container.logAction$);

So that means:

  • when userContainerDOM$ is subscribed it creates a new UserContainer component (from the userContainer$);
  • when userContainerLogAction$ is subscribed it creates a new UserContainer component (from the userContainer$);

Which means that each stream has access to a different "instance" of UserContainer and that the log action we retrieve does not come from the same component as the DOM.
In other words: we subscribe to events of a component that is not displayed on screen and, as such, does not receive any DOM event**.
**this last statement is only true because the UserContainer is isolated (see @cycle/isolate)

Now the shareReplay(1) solution transforms the observable into a hot observable. This means the whole subscription sequence will only be executed once per value produced by the stream user$. We also keep the last produced value for further subscriber to get that value when they subscribe.
Which means that now, we are only creating a single UserContainer per value produced by the user$ stream and that all the underlying subscribers will work on the same "instance" of that UserContainer.

Conclusion

Cycle.js beginners need to overcome a pretty huge obstacle before embracing the power of Cycle.js: RxJS. Don't get me wrong, RxJS is a great library it's just not perfectly suited for the idea behind Cycle.js.

There is very little to learn about Cycle.js, it is more of an idea more than a complete library/framework. Once you get the idea behind Cycle.js (which is pretty easy) what you have to learn is RxJS. Be sure you know the different operators (at least those inside rx.lite.js) and, most importantly, be sure that you understand what are observables by reading this introduction to observables.

One last thing, don't hesitate to ask questions on the Cycle.js's gitter chan ;)