状態管理ツールdobについて

[dobjs/dob](https://github.com/dobjs/dob) についての解説です。

## dobとは
`Light and fast 🚀 state management tool using proxy` と書かれているように、軽くて速い状態管理ツールです。
特定のフレームワークに依存することなく使えます。
現状フロントエンドの状態管理ツールは、特定のフレームワークに依存しているものが殆どですが、このツールをうまく利用すれば、Viewフレームワークだけを取り替えることが出来ると考えています。もちろんサーバー側でも使えます。


## dobのバックグラウンド
まずは、dobで利用されているJavaScriptの標準機能[Proxy](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy)についてざっくり説明します。

（ここは表面的な部分からは触れないので、読み飛ばしても大丈夫です）

Proxy オブジェクトとは、プロパティの検索・代入・削除等の操作をしたときに、独自の動作を定義することが出来るものです。


例として、猫の体重をチェックするコードは以下のようになります。

```javascript
{
  const handler = {
    set (target, prop, value) {
      if (value < 10) {
        console.log(`現在${value}kgです。`)
        target[prop] = value
        return true
      }
      
      throw new Error(`食事内容を変更してください。`)
    }
  }
  
  const cat = new Proxy({ weight: 2 }, handler)
  
  cat.weight += 1 // 現在3kgです。
  cat.weight += 2 // 現在5kgです。
  cat.weight += 10 // Error: 食事内容を変更してください。
}
```

この機能を利用し、値の変更を検知をすることが出来るツールです。


## dobのざっくり簡単な使い方
`observable` でobjectを監視対象とし、 `observe` でpropertyの変更を検知し処理を実行します。

```javascript

{
  const { observable, observe, Action } = dob
  
  const dog = observable({
    name: 'ポチ',
    gender: 'male',
    age: 2,
    condition: {
      happy: true,
      hungry: false      
    }
  })
  
  // observe定義時に、渡した関数が即時実行されます。
  observe(() => {
    console.log(dog.name, dog.gender)
    
    if (dog.condition.hungry) {
      console.log('ご飯を食べさせましょう。')
    }
  })
  // ポチ male
  
  
  // observe内でアクセスされたpropertyを変更すると検知対象となります。
  dog.name = 'ポチ太郎'
  // ポチ太郎 male
  
  // observe内で参照されていないpropertyを変更しても何も起きません。
  dog.age += 1
  
  // Actionを使うことで、値を複数回変更しても、コールバックが一度のみ実行されるようになります。
  Action(() => {
    dog.name = 'ポチ美'
    dog.gender = 'female'
  })
  // ポチ美 female
  
  
  // object を持つ property についても同様に扱えます。
  dog.condition.hungry = true
  // ポチ美 female
  // ご飯を食べさせましょう。
}
```

上記のように、property値の変更をすることで、コールバックが呼び出されていることが確認できるかと思います。

## 気を付ける点
自分がはまったことのある点をいくつか紹介します。

### 配列操作

以下のように、配列自体を監視すると、配列に値を追加してもコールバックが呼ばれません。

```javascript
{
  const { observable, observe, Action } = dob
  
  const shop = observable({
    fruits: ['🍎', '🍊', '🍌']
  })
  
  observe(() => {
    console.log(shop.fruits, 'フルーツが変更されました')
  })
  // Proxy {}, フルーツが変更されました。
  
  // 以下を実行しても、配列自体は同じものと見なされるため、コールバックが実行されない。
  shop.fruits.push('🍓')
}
```

上記のケースの場合は、`shop.fruits.length` を参照する等、配列操作によって変更される部分を監視する必要があります。

### 外部関数

`observe` の中で別の関数を呼び出しても、その関数で使われたpropertyは検知対象となります。

```javascript
{
  const { observable, observe, Action } = dob
  
  const tree = observable({
    root: 1,
    leaf: 100
  })
  
  function treeNode () {
    return tree.root + tree.leaf
  }
  
  observe(() => {
    console.log(tree.root, treeNode())
  })
  // 1 101
  
  // treeNodeで参照しているため、以下の処理は検知対象となる。
  ++tree.leaf
  // 1 102
}
```

仕組みを理解すると当然と言えば当然なんですが、外側の関数は割と見落としがちなので、注意が必要です。
検知対象から外したい場合は、`Action(fn)` を呼び出すことで対象外に出来ます。

### 自己検知による無限ループ

observe時に、自分自身が参照している値を変更すると無限ループになります。

```javascript
{
  const { observable, observe, Action } = dob
  
  const game = observable({
    count: 1
  })
  
  observe(() => {
    ++game.count // InternalError: too much recursion
  })
}
```

observe時に値を変更したい場合は、以下のように `Action` でくくる必要があります。（[参考issue](https://github.com/dobjs/dob/issues/8)）


```javascript
{
  const { observable, observe, Action } = dob
  
  const game = observable({
    count: 1
  })
  
  observe(() => {
    Action(() => {
      ++game.count // no loop
    })
  })
}
```

### 検知対象とならないproperty

検知対象となるルールとして、observeに渡したコールバック関数が、最後に実行されたときに読み取られたpropertyが検知対象となります。

以下では、分岐により読み取られなかったpropertyを変更しても実行されない例です。

```javascript
{
  const { observable, observe, Action } = dob
  
  const game = observable({
    isPlay: false,
    title: '🦍ワールド'
  })
  
  observe(() => {
    console.log(game.isPlay ? 'ゲームプレイ中' : 'ゲームしてません')
                
    if (game.isPlay) {
      console.log(`「${game.title}」をプレイしています。`)
    }
  })
  // ゲームしてません
  
  // 以下のproperty変更は、最後に呼び出されたコールバックで読み取られていないため、呼び出されません。
  game.title = '🐈の大冒険'
  
  game.isPlay = true
  // ゲームプレイ中
  // 「🐈の大冒険」をプレイしています。
  
  // 以下のproperty変更は、最後に呼び出されたコールバックで読み取られたため、呼び出されます。
  game.title = '🐶クエスト'
  // ゲームプレイ中
  // 「🐶クエスト」をプレイしています。
}
```

これらを考慮するのは大変なため、分岐で使ったpropertyは変更の有無に関わらず問題が無いような作りを考えたほうが楽です。
自分はコールバックの頭に `void (game.title, ...)` のように、監視したいpropertyを並べて、分岐による処理がされないように施したりしています。

## 試してみる

上記のコードは https://observablehq.com/@ampcpmgp/dob-example に置いてあります。devtoolsのconsole画面を開いて、画面内コードセル上にある右上の実行ボタンを押すことで、実際の動作を確認できます。編集して実行しても大丈夫です。（Observableもとても良いツールなので使ってみてください😃）


## 所感

objectの変更検知が、他ツールに比べて直感的でわかりやすいと思います。
特定のフレームワークへの依存性が小さいという点も、本来のエコシステムにより近づくように思います。

ちょっと古い記事ですが、[jsのProxyを利用した状態管理ツールを試してみた](https://qiita.com/am_nimitz3/items/f9f4999115d648f4b83f)で他にも軽く試したことがあるので、良かったら参考にしてください。

以上です。