LoginSignup
13
9

More than 5 years have passed since last update.

preact + parket で軽量開発

Last updated at Posted at 2018-02-01

Parket

parket

inspired by mobx-state-tree な状態管理ライブラリです。preact と併用することで、hyperappに迫る軽量化を実現しています(Small (~1.5KB))。initial commit が 2018/01/24 と、出来たてホヤホヤですね。

usage

リポジトリを眺めていれば特に難しいことは無いと思います。parket に view は含まれておらず、現状では preact と react のバインディングが含まれています。以下リポジトリの usage 引用。

usage.js
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 出来ます。

サービス層に乗せてみる

Untitled.gif

「状態管理にサービス層がないとやっていけない」と最近思っている筆者は、さっそくasync/await に乗せてみました。SingleStoreを作って、サービス起動して、View を render するいつもの構成です。

app.js
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'))

model.js
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
    }
  })
})

service.js
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 })
}


view.js
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


難なくこなしてくれました。モデルが純粋で、魅力的 :sparkles:

sample: https://github.com/takefumi-yoshii/preact-parket-sandbox

Single source of truth(自説)

上記サンプルの app.js では Store を一つにしています。というのも、service.js を以下の様に変更すれば、ReduxAction と同じ様にonPatchStore()関数でグローバルイベントとしてキャッチすることができるためです。 Redux ユーザーにとっては、ピンとくる利点ではないでしょうか。

service.js
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 :innocent:

MobXらしい簡単さと、イベント駆動が融合していて、v0.2.0ですがとても期待の高まるライブラリです。

13
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
9