Parket

inspired by mobx-state-tree
な状態管理ライブラリです。preact と併用することで、hyperappに迫る軽量化を実現しています(Small (~1.5KB))。initial commit が 2018/01/24 と、出来たてホヤホヤですね。
usage
リポジトリを眺めていれば特に難しいことは無いと思います。parket に view は含まれておらず、現状では preact と react のバインディングが含まれています。以下リポジトリの usage 引用。
import model from 'parket';
// model returns a "constructor" function
const Person = model('Person', { // name is used internally for serialisation
initial: () => ({
firstname: null,
lastname: null,
nested: null,
}),
actions: state => ({
setFirstName (first) {
state.firstname = first; // no set state, no returns to merge, it's reactive™
},
setLastName (last) {
state.lastname = last;
},
setNested (nested) {
state.nested = nested;
},
}),
views: state => ({
fullname: () => `${state.firstname} ${state.lastname}`, // views are computed properties
}),
});
views
は Vue.js や Mobx の computed
と等価のものですね。(是非 computed に改名して欲しい)
mittで動いている
軽量 EventEmitter の mitt が Dependencies。値の変化をsubscribeするAPIがいくつか用意されている様です。console の内容を見ると、イベント名 や payload が乗っかってるのが分かります。
#counterA.onAction(console.log)
{name: "increment", path: "/counterA", args: Array(1)}
# counterA.onPatch(console.log)
{path: "/counterA/count", op: "replace", value: 1}
# counterA.onSnapshot(console.log)
{name: "counterA", count: 1, label: "counterA + counterB", calcVal: 0, __p_model: "Counter"}
これらは subscribe
の wrapper で、戻り値を呼ぶことで unsubscribe 出来ます。
サービス層に乗せてみる
「状態管理にサービス層がないとやっていけない」と最近思っている筆者は、さっそくasync/await に乗せてみました。SingleStoreを作って、サービス起動して、View を render するいつもの構成です。
import 'babel-polyfill'
import { render } from 'preact'
import model from 'parket'
import Counter from './model'
import runService from './service'
import View from './view'
const store = model('Store', {
initial: () => ({
counterA: Counter({
name: 'counterA',
label: 'counterA + counterB',
count: 0
}),
counterB: Counter({
name: 'counterB',
label: 'counterA x counterB',
count: 1
})
})
})()
runService(store)
render(<View store={store} />, document.getElementById('app'))
import model from 'parket'
export default model('Counter', {
initial: () => ({
name: '',
count: 0,
label: '',
calcVal: 0
}),
actions: state => ({
increment () {
state.count++
},
decrement () {
state.count--
},
setCalcVal (calcVal) {
state.calcVal = calcVal
}
})
})
export function change () {
return Promise.race(
[].slice.call(arguments).map(model => {
return new Promise(resolve => {
const unsubscribe = model.onPatch(event => {
unsubscribe()
resolve(event)
})
})
})
)
}
async function calculate ({ counterA, counterB }) {
while (true) {
counterA.setCalcVal(counterA.count + counterB.count)
counterB.setCalcVal(counterA.count * counterB.count)
await change(counterA, counterB)
}
}
export default function runService (store) {
const { counterA, counterB } = store
calculate({ counterA, counterB })
}
import { Component } from 'preact'
import { connect, observe, Provider } from 'parket/preact'
function MyComponent ({ name, count, label, calcVal, increment, decrement }) {
return (
<div>
<h1>{name}:{count}</h1>
<p>{label} = {calcVal}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
)
}
@observe
class Panel extends Component {
render ({ counter }) {
return (
<MyComponent
name={counter.name}
count={counter.count}
label={counter.label}
calcVal={counter.calcVal}
increment={counter.increment}
decrement={counter.decrement}
/>
)
}
}
@connect
class Container extends Component {
render ({ store }) {
return <div>{this.props.children}</div>
}
}
const root = ({ store }) => (
<Provider store={store}>
<Container>
<Panel counter={store.counterA} />
<Panel counter={store.counterB} />
</Container>
</Provider>
)
export default root
難なくこなしてくれました。モデルが純粋で、魅力的
sample: https://github.com/takefumi-yoshii/preact-parket-sandbox
Single source of truth(自説)
上記サンプルの app.js
では Store を一つにしています。というのも、service.js
を以下の様に変更すれば、ReduxAction と同じ様にonPatchStore()
関数でグローバルイベントとしてキャッチすることができるためです。 Redux ユーザーにとっては、ピンとくる利点ではないでしょうか。
function onPatchStore (event) {
// console.log(event) # do something
}
export default function runService (store) {
const { counterA, counterB } = store
calculate({ counterA, counterB })
store.onPatch(onPatchStore) // New
}
LGTM
MobXらしい簡単さと、イベント駆動が融合していて、v0.2.0ですがとても期待の高まるライブラリです。