/caballo-vivo

Glue code for pure RxJS applications to connect with React-Router, and other operators

Primary LanguageJavaScript

Caballo Vivo

1177. los cementerios no deben estrenarse enterrando un hombre muerto sino un caballo vivo
— CJC

Thin, opinionated layer for pure RxJS applications to connect with React-Router (or any router using history). It has the advantage of scaling well, allowing more concise and expressive code and a better separation between business logic and view layer.

Here’s the equivalent implementation of the Redux Reddit advanced tutorial with caballo-vivo, in less than 100 lines of code (online sandbox):

// type localStorage.setItem('cv-log', true) in the console to see the logs!
import React from "react"
import { render } from "react-dom"
import { OrderedMap, Map } from "immutable"
import { partialRight } from "ramda"
import { Subject, merge, concat, of, from } from "rxjs"
import { map, catchError, switchMap } from "rxjs/operators"
import { Router, Switch, Route, Link } from "react-router-dom"
import {
  createLocation$,
  createNavigateTo$,
  createStore$,
  history,
  flog,
  stow
} from "@zambezi/caballo-vivo"
import ClipLoader from "react-spinners/ClipLoader"
import "./index.css"

const getPosts$ = new Subject()

const pathToIntent = OrderedMap([
  ["/:subreddit", ({ subreddit }) => getPosts$.next({ subreddit })],
  ["/", () => getPosts$.next({ subreddit: "all" })]
])

const location$ = createLocation$(pathToIntent).pipe( // (1)
  map(location => state => state.set("location", location))
)

const redditJourney$ = getPosts$.pipe( // (2)
  switchMap(({ subreddit }) =>
    concat(
      of(state => state.set("loading", true)), // (3)
      from(fetch(`https://www.reddit.com/r/${subreddit}.json`)).pipe(
        switchMap(res =>
          from(res.json()).pipe(
            map(json => json.data.children.map(child => child.data))
          )
        ),
        stow("subreddit") // (4)
      ),
      createNavigateTo$(subreddit), // (5)
      of(state => state.set("loading", false))
    )
  ),
  catchError(() =>
    of(state => state.set("error", "This subreddit is not available"))
  )
)

merge(location$, redditJourney$)
  .pipe(createStore$(Map()), flog("Render state"), map(toView)) // (6)
  .subscribe(partialRight(render, [document.getElementById("root")]))

function toView(state) { // (7)
  if (state.has("error")) return <p>{state.get("error")}</p>
  if (state.get("loading"))
    return (
      <div className="loader">
        <ClipLoader />
      </div>
    )
  return (
    <Router history={history}>
      <section className="links">
        {["all", "reactjs", "rxjs", "javascript", "node"].map(l => (
          <Link key={l} to={l}>{`r/${l}`}</Link>
        ))}
      </section>
      <hr />
      <Switch>
        <Route path="/:subreddit">
          <ul>
            {state.get("subreddit").map(post => (
              <li key={post.id}>
                <a href={post.url} target="_blank" rel="noopener noreferrer">
                  {post.title}
                </a>
              </li>
            ))}
          </ul>
        </Route>
      </Switch>
    </Router>
  )
}
  1. Passing a map to createLocation$ will block the hisory when the route changes and the new url matches one of the keys.

  2. We listen to the getPosts$ intent, triggered by the routes above, to kickstart a complex series of asynchronous events (set/unset loader flag, download subreddit).

  3. When we emit reducer functions, they get executed by the store (see below). All that a reducer function does is get the current state snapshot and return a new snapshot.

  4. stow is just an operator which emits a reducer function to set the value received on a key or path in the store. This pattern is so common that it got its own operator.

  5. createNavigateTo$ unblocks the history and changes the address in the URL bar. At this point, all the routes matching the new address can render.

  6. createStore$ creates a store, which is an rxjs operator that has an initial value, takes reducers function in and spits state snapshots out.

  7. The view function is just a function which takes state snapshots in and generates JSX. Views and state / business logic are decoupled and interchangeable.

For a more advanced example application, please check: https://github.com/gabrielmontagne/fa-doodle