Hi Chet,

And thanks for the feedback — those are very good questions and I face them almost daily. Dynamic lists and their performance is one of the toughest things in unidirectional dataflow architectures. Usually the solution depends on the problem/domain so it’s hard to give you any silver bullets. But yes, there are some patterns that apply to many cases!

Virtual DOM rendering shouldn’t be an issue, ‘cause combinators handle stream subscriptions, duplicates and disposing automatically under the hood. Of course adding / removing items cause whole list re-rendering but it can be avoided by using component key and shallow props diff on shouldComponentUpdate— stream identities don’t change so React can do the optimization:

export const pure = render => React.createClass({
shouldComponentUpdate(nextProps) {
return !shallowEq(this.props, nextProps)
},
render() { return render(this.props) }
})
// usage
const MyComponent = pure(props => <div>{...}</div>)

It’s also worth to note that with combinators, each Combinator elements “stops” the stream flattening and tells React’s Reconciler to render stream values instead of propagating them to upper streams. VDOM starts to kind of live its “own life” under combinator and same applies also to upper stream. Basically this means that if the item streams receive new values, only the list item component re-renders, thus giving you some performance benefits. If the list stream changes, the whole list re-renders (thus all item components re-render), which is not good.


React Combinators were a first draft so they were not perfect — mixing components, streams and function calls lead to situations where it was hard to justify/follow the evaluation order of the app code. I’d suggest to take a look at react-reactive-toolkit: it’s a successor of combinators that we’ve been using few months now and it has worked amazingly well. In practice we are now writing only pure React components that take streams as props and render them by using toolkit’s “reactive elements”:

const TimeComponent = ({time}) =>
<div>
<h1>Time component!</h1>
<R.p>
Time is now {time}
</R.p>
</div>

Now we can use that component with or without streams:

const App = () => {
const now = new Date().toTimeString()
const time$ =
Observable.interval(1000)
.startWith(null)
.map(() => new Date().toTimeString())

return <div>
<TimeComponent time={now} />
<TimeComponent time={time$} />
</div>
}

Your concern about “stream of streams” is still valid: deriving the state usually gets messy when you have multiple levels of nesting in your streams (you’ll probably end up doing a lot of combining and flattening so that you can extract state from your nested streams).

We’ve tackled this issue by using “atoms” and reactive lenses. Personally I’m very excited about atoms and reactive lenses — they are extremely powerful tool to reduce boilerplate and create concise reactive state graphs.

Don’t be scared of the formal definition! Atom is basically just an Rx.ReplaySubject / Bacon.Bus / Kefir.pool that can create “sub-atoms” via lenses. Lens is kind of binding between the lensed (sub-)atom and its parent: lensed atom sees only the lensed part of the parent and and can modify it and the modifications apply to the parent (and vice versa).

In practice you can define your ”application state” as a single atom and treat it like normal stream (and naturally use higher order functions to derive streams from the state), but you can also divide it into smaller parts by using lenses and modify those “sub-states” and whole time your “application state” stays in sync because of lenses (see for example this).


But back to the original problem: list rendering optimizations. We know that:

  1. Streams preserve their identity
  2. We can prevent re-render of list components by using component key and shallow props diff
  3. We can have our application state object in a single stream (atom) and split it to sub-streams (atoms) so that sub-streams and app-state stream stay in sync
  4. Streams have .scan() and .skipDuplicates() and .map() higher order functions

First we have to somehow transform our “Stream[List[value]]” into “Stream[List[Stream[value]]]” so that we can optimize list item rendering. By using 3. and 4. we can create such function: liftListById

Then we can create our application state as an atom and use fact 3. and our new function to lift it:

import Kefir from "kefir"
import atom from "kefir.atom"
import {liftListById} from "./lifting"

// ---- model ----
let ids = 0

const BMIs = () => {
const counters = atom([])

const add = (weight, height) =>
counters.modify(bmis => [...bmis, {id: ++ids, weight, height}])

const remove = id =>
counters.modify(bmis => bmis.filter(b => b.id !== id))

const counters$$ = liftListById(counters, (id, counter) => {
const weight = counter.lens(R.lensProp("weight"))
const height = counter.lens(R.lensProp("height"))
const bmi = Kefir.combine([weight, height])
.map(([w, h]) => Math.round(w / (h * h * 0.0001)))
.toProperty()
return {id, weight, height, bmi, remove: () => remove(id)}
})
return {counters, add, counters$$}
}

And then we can define your UI by using this model:

import React from "react"
import RR from "react.reactive"
import R from "ramda"
import Kefir from "kefir"
import {render} from "react-dom"

const $app =
document.getElementById("app")
const App = ({model: {counters, counters$$}}) => {
const avgBy = (prop, vals) =>
R.sum(R.map(R.prop(prop), vals)) / vals.length
const avgWeight = counters
.map(counters => counters.length === 0
? "--"
: avgBy("weight", counters).toFixed(1))
.toProperty()

return <div>
<RR.h1>BMI counterz (avg. weight: {avgWeight})</RR.h1>
<RR.div>
{counters$$.map(counters => counters.map(counter =>
<Counter key={counter.id} counter={counter}/>
))}
</RR.div>
<button onClick={() => model.add(80, 180)}>Add</button>
</div>
}

const Counter = ({counter: {id, weight, height, bmi, remove}}) => {
return <div>
<Slider title="Weight" range={[40, 150]} value={weight}/>
<Slider title="Height" range={[100, 200]} value={height}/>
<RR.div>BMI is: {bmi}</RR.div>
<div>
<button onClick={remove}>Remove</button>
</div>
<hr />
</div>
}


const Slider = ({title, range: [min, max], value}) =>
<RR.div>
{title}
<RR.input type="range"
min={min}
max={max}
value={value}
onChange={e => value.set(Number(e.target.value))}/>
{value}
</RR.div>

const model = BMIs()
render(<App model={model}/>, $app)

And if we want to give a finalizing touch to our perf (remove unnecessary item re-renders from list re-renders), we can use facts 1. and 2. and the “pure” function we defined before.

Thus, the whole application looks like this. You can run it and see console log lines which parts of the application get (re)-rendered when you interact with the app. Results are pretty good. ;-)


Well… that may look a bit complicated at first glance but after you get used to lenses, you can build massive, complex and robust UIs in no time. Note that pure and liftListBy functions are component/model agnostic so you can apply them to wherever you like.