8

I’m new to reactive programming and toying around with cycle.js, trying to implement who to follow box from this tutorial. But I understood that for proper implementation (and learning purposes) I don’t have one piece of data: full user name. I can get it by sequentially getting users and then full user data from server. In imperative style I would do something like this:

fetch(`https://api.github.com/users`)
  .then(data => data.json())
  .then(users => fetch(users[0].url))
  .then(data => data.json())
  .then(/* ... work with data ... */)

But how do I do it in cycle? I’m using fetch driver and trying something like this:

function main({ DOM, HTTP }) {
  const users = `https://api.github.com/users`;

  const refresh$ = DOM.select(`.refresh`).events(`click`)

  const response$ = getJSON({ key: `users` }, HTTP)

  const userUrl$ = response$
    .map(users => ({
      url: R.prop(`url`, R.head(users)),
      key: `user`,
    }))
    .startWith(null)

  const request$ = refresh$
    .startWith(`initial`)
    .map(_ => ({
      url: `${users}?since=${random(500)}`,
      key: `users`,
    }))
    .merge(userUrl$)

  const dom$ = ...

  return {
    DOM: dom$,
    HTTP: request$,
  };
}

where getJSON is

function getJSON(by, requests$) {
  const type = capitalize(firstKey(by));

  return requests$
    [`by${type}`](firstVal(by))
    .mergeAll()
    .flatMap(res => res.json());

And I’m always getting some cryptic (for me) error like: TypeError: Already read. What does it mean and how do I handle it properly?

André Staltz
  • 13,304
  • 9
  • 48
  • 58
alice kibin
  • 572
  • 1
  • 4
  • 18

2 Answers2

9

You were quite close. You just need to remove startWith(null) as a request, and grabbing the second response (you were missing the getJSON for that one).

function main({ DOM, HTTP }) {
  const usersAPIPath = `https://api.github.com/users`;

  const refresh$ = DOM.select(`.refresh`).events(`click`);

  const userResponse$ = getJSON({ key: `user` }, HTTP);
  const listResponse$ = getJSON({ key: `users` }, HTTP);

  const userRequest$ = listResponse$
    .map(users => ({
      url: R.prop(`url`, R.head(users)),
      key: `user`,
    }));

  const listRequest$ = refresh$
    .startWith(`initial`)
    .map(_ => ({
      url: `${usersAPIPath}?since=${Math.round(Math.random()*500)}`,
      key: `users`,
    }));

  const dom$ = userResponse$.map(res => h('div', JSON.stringify(res)));

  return {
    DOM: dom$,
    HTTP: listRequest$.merge(userRequest$),
  };
}
André Staltz
  • 13,304
  • 9
  • 48
  • 58
  • Thanks, it works! But it still seems kind of magic to me though. Do I understand correctly that `userRequest$` just waits for `listResponse$` to emit anything and then just do what’s next in chain? – alice kibin Nov 23 '15 at 16:16
  • 1
    Correct. It maps an event from `listResponse$` to a new request. – André Staltz Nov 23 '15 at 20:17
  • Understood. Thanks again, I really like cycle.js and will continue to dig into it. – alice kibin Nov 23 '15 at 20:25
  • I finally figured out the cyclejs HTTP driver used here is the @cycle/fetch driver: https://github.com/cyclejs/cycle-fetch-driver – bloodyKnuckles May 15 '16 at 22:26
0

Because inquiring minds want to know...here's a complete working example:

import Cycle from '@cycle/rx-run';
import {div, button, makeDOMDriver} from '@cycle/dom';
import {makeFetchDriver} from '@cycle/fetch';
import R from 'ramda'

function main({DOM, HTTP}) {

  const usersAPIPath = 'https://api.github.com/users';
  const refresh$ = DOM.select('button').events('click');

  const userResponse$ = getJSON({ key: 'user' }, HTTP);
  const listResponse$ = getJSON({ key: 'users' }, HTTP);

  const userRequest$ = listResponse$
    .map(users => ({
      url: R.prop('url', R.head(users)),
      key: 'user',
    }));

  const listRequest$ = refresh$
    .startWith('initial')
    .map(_ => ({
      url: `${usersAPIPath}?since=${Math.round(Math.random()*500)}`,
      key: 'users',
    }));

  const dom$ = userResponse$.map(res => div([
    button('Refresh'),
    div(JSON.stringify(res))
  ]));

  return {
    DOM: dom$,
    HTTP: listRequest$.merge(userRequest$)
  };

  function getJSON(by, requests$) {
    return requests$.byKey(by.key)
      .mergeAll()
      .flatMap(res => res.json());
  }
}

Cycle.run(main, {
  DOM: makeDOMDriver('#main-container'),
  HTTP: makeFetchDriver()
});

It took me a while to figure out HTTP was the @cycle/fetch driver, and NOT the @cycle/http driver. Next, a bit of searching turned the ramda npm library providing prop and head methods.

bloodyKnuckles
  • 11,551
  • 3
  • 29
  • 37