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