2

I have been attempting to create some cycle.js examples as nested dialogues, and switching between them using a select box.

One of the dialogues is a clone of the official Github HTTP Search example.

The other dialogue a more basic one which does not have HTTP, only DOM.

I feel like I wrapped my head around switching between the 2, but I'm fairly new to Rx so that may be done incorrectly or naively.

It all seemed to work well until I added a loading indicator to the search page.

To do that, I turned this:

  const vTree$ = responses.HTTP
    .filter(res$ => res$.request.indexOf(GITHUB_SEARCH_API) === 0)
    .flatMapLatest(x => x)
    .map(res => res.body.items)
    .startWith([])
    .map(results =>
      h('div.wrapper', {}, [
        h('label.label', {}, 'Search:'),
        h('input.field', {type: 'text'}),
        h('hr'),
        h('section.search-results', {}, results.map(resultView)),
      ])
    )

Into this:

  const searchResponse$ = responses.HTTP
    .filter(res$ => res$.request.indexOf(GITHUB_SEARCH_API) === 0)
    .flatMapLatest(x => x)
    .map(res => res.body.items)
    .startWith([])

  const loading$ = searchRequest$.map(true).merge(searchResponse$.map(false))

  // Convert the stream of HTTP responses to virtual DOM elements.
  const vtree$ = loading$.withLatestFrom(searchResponse$, (loading, results) =>
      h('div.wrapper', {}, [
        h('label.label', {}, 'Search:'),
        h('input.field', {type: 'text'}),
        h('span', {}, loading ? 'Loading...' : 'Done'),
        h('hr'),
        h('section.search-results', {}, results.map(resultView)),
      ])
    )

I now have 2 issues

  1. The 'checkbox value set to' and 'route changed' messages are logged twice for every change of the checkbox.
  2. The HTTP request log only fires once, but if you watch the network activity in Dev Tools you'll see two GET requests simultaneously.

Thanks for any help!

EDIT: Solved my own problem. See answer below.

SkaterDad
  • 71
  • 7
  • You are using `loading$.withLatestFrom()`, so it will only produce a new value when `loading$` has a new value. Try `combineLatest()`. – Frederik Krautwald Dec 04 '15 at 20:18
  • I had originally created an intermediate observable `state$ = Rx.Observable.combineLatest(searchResponse$, loading$, (resp, loading) => { return {results: resp, loading: loading}})`. It worked exactly the same, since loading$ changes every time a response comes in. The loading indicator works perfectly fine, the issue is that my HTTP requests are firing off 2 times instead of just 1. If you remove the loading indicator code, the requests fire once like they're supposed to. I feel like it is related to how I'm implementing the nested dialogues. – SkaterDad Dec 05 '15 at 03:27

1 Answers1

5

I ended up solving this problem by rebuilding my entire application from scratch until I found the breaking point.

What I learned is that you need to add .share() to any observable stream which will be subscribed/mapped/etc by more than one downstream observable.

  const searchRequest$ = DOM.select('.field').events('input')
    .debounce(500)
    .map(ev => ev.target.value.trim())
    .filter(query => query.length > 0)
    .map(q => GITHUB_SEARCH_API + encodeURI(q))
    .share() //needed because multiple observables will subscribe

  // Get search results from HTTP response.
  const searchResponse$ = HTTP
    .filter(res$ => res$ && res$.request.url.indexOf(GITHUB_SEARCH_API) === 0)
    .flatMapLatest(x => x) //Needed because HTTP gives an Observable when you map it
    .map(res => res.body.items)
    .startWith([])
    .share() //needed because multiple observables will subscribe

  //loading indication.  true if request is newer than response
  const loading$ = searchRequest$.map(true).merge(searchResponse$.map(false))
    .startWith(false)
    .share()

  //Combined state observable which triggers view updates
  const state$ = Rx.Observable.combineLatest(searchResponse$, loading$,
    (res, loading) => {
      return {results: res, loading: loading}
    })

  //Generate HTML from the current state
  const vtree$ = state$
    .map(({results, loading}) =>
      .........
SkaterDad
  • 71
  • 7
  • 1
    Correct, that's what "share" means. They are called *hot* observables. Read more here https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables – André Staltz Dec 13 '15 at 21:14