Skip to content

Instantly share code, notes, and snippets.

@milankinen
Created March 2, 2016 21:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milankinen/a3da18f4e7d551523392 to your computer and use it in GitHub Desktop.
Save milankinen/a3da18f4e7d551523392 to your computer and use it in GitHub Desktop.
Optimized list rendering with atoms and lenses
import React from "react"
import R from "ramda"
const shallowEq = (a, b) => {
const aKeys = R.keys(a), bKeys = R.keys(b)
if (aKeys.length !== bKeys.length) return false
for (let i = 0; i < aKeys.length; i++) {
if (a[aKeys[i]] !== b[aKeys[i]]) return false
}
return true
}
const subLens = (by, val, def) => {
const orDefault = val => val === undefined ? def : val
const matches = R.compose(R.equals(val), by)
return R.lens(
s => orDefault(R.find(matches, s)),
(a, s) => {
const idx = R.findIndex(matches, s)
return idx >= 0 ? R.set(R.lensIndex(idx), a, s) : s
})
}
// liftListBy :: (s -> a) -> Atom [{a,b}] -> (a -> Atom b -> Atom c) -> Atom [Atom(c)]
export const liftListBy = R.curry((by, listAtom, subModel, defaultValue = {}) =>
listAtom
.scan((cache, m) => {
const itemsByKey = R.indexBy(by, m)
const added = m.map(by).filter(key => !(key in cache)).map(key =>
subModel(key, listAtom.lens(subLens(by, key, defaultValue))))
const removed =
R.keys(cache).filter(key => !(key in itemsByKey))
return R.isEmpty(added) && R.isEmpty(removed) ? cache : R.pipe(
R.merge(R.indexBy(by, added)),
R.omit(removed)
)(cache)
}, {})
.skipDuplicates()
.map(R.values)
.toProperty())
export const liftListById = liftListBy(R.prop("id"))
export const pure = render => React.createClass({
shouldComponentUpdate(nextProps) {
return !shallowEq(this.props, nextProps)
},
render() { return render(this.props) }
})
import React from "react"
import RR from "react.reactive"
import R from "ramda"
import Kefir from "kefir"
import atom from "kefir.atom"
import {render} from "react-dom"
import {liftListById, pure} from "./fun"
const $app =
document.getElementById("app")
// ---- 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$$}
}
// ---- controller-view ----
const App = pure(({model: {counters, counters$$}}) => {
console.log("render app")
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 => {
console.log("render list with", counters.length, "counters")
return counters.map(counter =>
<Counter key={counter.id} counter={counter}/>
)
})}
</RR.div>
<button onClick={() => model.add(80, 180)}>Add</button>
</div>
})
const Counter = pure(({counter: {id, weight, height, bmi, remove}}) => {
console.log("render counter", id)
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment