Vue Composition APIでストアパターンをスマートに使って状態管理をする

# TL;DR
- **[PR]** Reactの状態管理ライブラリ「[unstated-next](https://github.com/jamiebuilds/unstated-next#migration-from-unstated)」をVue Composition APIベースに移植したよ
- 状態をComposition APIで共有し、Read/Writeできるので便利だよ
- 特定のコンポーネントツリーでしか利用しないようにスコーピングができるよ
- 型もばっちり効くよ

----
Vue Composition APIのRFCも[マージされ](https://github.com/vuejs/rfcs/pull/78)、Vue3のalphaもひっそりと[リリースされて](https://www.npmjs.com/package/vue/v/3.0.0-alpha.0)いて、もういくつ寝るとVue3！という雰囲気になってきました。多くのVue.jsユーザがComposition APIの実戦投入について検討をしたり、それに向けた素振りをしているのではないかと思います。今年のアドベントカレンダーでも多くのアドベントカレンダーがComposition APIに触れていたり、いくつかの記事がストアの設計を絡めていて、自分も記事を横にサンプルコードを書いてみたりしました。

ところで、hooksで先行しているReactには[unstated-next](https://github.com/jamiebuilds/unstated-next#migration-from-unstated)という**必要最低限の実装**(なんとTSで38行！)**でとてもシンプルなライブラリがあります**(使い方や仕組みは後述します)。過日業務で使ってみたのですが、導入したエンジニアのアツい推薦も納得するほどの使い勝手の良さでした。

Vueスタックでも同様の状態管理を行えたらなとぼんやりと思っていたのですが、特に同様のライブラリが存在しないようだったのと、Composition APIのドキュメントを読んでいる際に**移植が可能だとわかったので作ってみました**。

**[Github]** : https://github.com/resessh/vue-unstated
**[npm]** : https://www.npmjs.com/package/vue-unstated

以下ライブラリの宣伝をしながら解説をする記事になります :pray: 

# なぜComposition APIだけではだめなのか
※ Composition APIを理解している方は読み飛ばしてください。

Composition APIはそもそも**ロジックの再利用性を高めることを目的とした仕様**です。例えば以下のようなComposition Functionがあるとします。

```js:use/counter.js
export const useCounter = () => {
  // 状態
  const state = reactive({ count: 0 })

  // 状態を変更する関数
  const increment = () => {
    state.count++
  }

  return {
    state,
    increment,
  }
}
```

上記のComposition Functionをコンポーネントで使うには以下のように呼び出します。

```vue:App.vue
<template>
  <div>
    <!-- クリックしたらカウントを増やす -->
    <button @click="increment">+</button>
    <!-- カウントを表示する -->
    <p>{{ count }}</p>
  </div>
</template>

<script>
import { useCounter } from 'use/counter'

export default {
  setup() {
    // Composition Functionを実行し、初期化された状態とメソッドを取り出す
    const { state, increment } = useCounter()

    // templateに状態・メソッドを渡す
    return {
      count: state.count,
      increment,
    }
  }
}
</script>
```

使う際に `useCounter()` でComposition Functionを実行していることから察せられるように、各コンポーネントで `useCounter` を利用しても**状態は共有されず**、それぞれのコンポーネント内で別々のインスタンスを初期化しているような動作になります。Composition APIは上記コードのとおり、状態とロジックを再利用可能な形で切り出すことがとてもきれいにできる反面、切り出した状態を共有する仕組みは提供されていません。
つまり、このカウンターの状態を共有したい場合は、初期化されたComposition Functionの中身を[^1]**何かしらの方法でコンポーネントをまたがって共有しなければなりません**。

[Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか？](https://qiita.com/tmy/items/a545e44100247c364a71) の記事では**これを実現するために、 Vueの [provide/inject](https://jp.vuejs.org/v2/api/index.html#provide-inject) というAPIを使っています**。このAPIは**「コンポーネントツリーのルートの方で`provide`したものは、同一コンポーネントツリー内であればどんなにルートから遠いコンポーネントでも `inject` だけで呼び出すことができる」**という機能を利用しています。

今回作った`vue-unstated`もこの `provide/inject` を利用していて、基本的に**同じ方法・方針で状態管理をしようとしています**。

[^1]: この場合 `useCounter()` で得られた `{ state, increment }` を指す。

# デモ
実際にコードを見たり触ったりできるデモはこちらになります。
[![Edit [vue-unstated DEMO] Todo](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/ugdg7-ugdg7?fontsize=14&hidenavigation=1&module=%2Fsrc%2Fuse%2Ftodos.js&theme=dark)

# vue-unstatedの使い方
ここからは上記デモのコードを使ってvue-unstatedの使い方について簡単に触れたいと思います。

## :one: Composition Functionをつくる
まずはVue Composition APIだけでComposition Functionを作ります。
今回はアイテムを登録することができるだけのTodoListを作ります。

```js:use/todos.js
import { reactive } from "@vue/composition-api"

const useTodos = () => {
  const state = reactive({
    items: [],   // Todoのアイテムの配列
    latestId: 0, // 最新のアイテムのid
  })

  const addItem = item => {
    state.latestId++
    // Todoは { id: number, title: string } の構造
    state.items.push({
      id: state.latestId,
      title: item.title
    })
  }

  return {
    items: state.items, // 共有したい状態はitemsだけなので、これだけexportする
    addItem,
  }
}
```

## :two: unstatedコンテナを作る
次に、 `vue-unstated` の `createContainer` でunstatedコンテナにしてexportします。

```diff:use/todos.js
+import { createContainer } from 'vue-unstated'
import { reactive } from "@vue/composition-api"

const useTodos = () => {
  const state = reactive({
    items: [],   // Todoのアイテムの配列
    latestId: 0, // 最新のアイテムのid
  })

  const addItem = item => {
    state.latestId++
    // Todoは { id: number, title: string } の構造
    state.items.push({
      id: state.latestId,
      title: item.title
    })
  }

  return {
    items: state.items, // 共有したい状態はitemsだけなので、これだけexportする
    addItem,
  }
}

+export default createContainer(useTodos)
```

## :three: 使いたいコンポーネントの親でprovideする
続けて、使いたいコンポーネントツリーのルートに近いコンポーネント(親側のコンポーネント)でコンテナを`provide`します。

```diff:App.vue
 <template>
   <div id="app">
     <todo-register/>
     <todo-list/>
   </div>
 </template>
 
 <script>
 import TodoRegister from "./components/TodoRegister.vue"
 import TodoList from "./components/TodoList.vue"
+import TodoContainer from "./use/todos"
 
 export default {
   name: "App",
   components: {
     TodoRegister,
     TodoList,
   },
   setup() {
     // const { items, addItem } = TodoContainer.provide() で即座に使うこともできます
+    TodoContainer.provide()
   }
 }
 </script>
```

## :four: 使いたいコンポーネントでuseContainerする
最後に使いたい子コンポーネントでコンテナを `useContainer` します。

__Todoアイテムのリスト__

```diff:components/TodoList.vue
 <template>
   <ul class="list">
     <li v-for="item in items" :key="item.id">
       <todo-item :item="item"/>
     </li>
   </ul>
 </template>
 
 <script>
 import TodoItem from "./TodoItem.vue"
+import TodoContainer from "../use/todos"
 
 export default {
   name: "TodoList",
   components: {
     TodoItem
   },
   setup() {
+    const { items } = TodoContainer.useContainer()
+    
+    return { items }
   }
 };
 </script> 
```

__Todoアイテムを登録するフォーム__

```diff:components/TodoRegister.vue
 <template>
   <form @submit.prevent="onSubmit">
     <input type="text" v-model="state.title">
     <button type="submit">add</button>
   </form>
 </template>
 
 <script>
 import { reactive } from "@vue/composition-api"
+import TodoContainer from "../use/todos"
 
 export default {
   name: "TodoRegister",
   setup() {
+    const { addItem } = TodoContainer.useContainer()
     const state = reactive({
       title: ""
     })
     const onSubmit = () => {
+      addItem({ title: state.title }) // Todoアイテムの登録
       state.title = ""
     }
 
     return {
       state,
       onSubmit
     }
   }
 }
 </script>
```

これだけでTodoアイテムを登録・閲覧できるようになります。
Vue Composition APIを使って**ロジックを切り出しただけの状態と比べてもほぼ差がない**のがわかるでしょうか?

# 動作の仕組み
本家`unstated-next`も38行と大変短いコードですが、ほぼ同様の実装である `vue-unstated` も34行とわずかなコードで動いています。
基本的な方針は上述の [Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか？](https://qiita.com/tmy/items/a545e44100247c364a71#%E5%85%84%E5%BC%9F%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E9%96%93%E3%81%A7%E3%82%B9%E3%83%88%E3%82%A2%E3%82%92%E5%85%B1%E6%9C%89) の 「兄弟コンポーネント間でストアを共有」の項で示されている **`provide/inject` でのストアオブジェクトの共有とほぼ同一**です。
強いて挙げるとすれば、`unstated`では**Composition Functionのインスタンスをまるごと共有しよう**という発想である点でしょうか？はじめて`unstated-next`のコードを読んだ時は、腰が抜けてイスから転げ落ちました。

興味がある方はぜひソースを読んでみてください。
https://github.com/resessh/vue-unstated/blob/master/src/index.ts

また、フィーチャーリクエストやコントリビューションもwelcomeですが、どうしてもプロジェクト内の固有の理由で変更を加えたい場合は、プロジェクトのリポジトリにコピペして編集してしまったほうが早いかもしれません[^2]。34行しかないし、アクティブに変更も無いのではないかと思っています。

[^2]: `unstated-next` を利用していたときは、`react-router`との兼ね合いで、コードを変更せざるを得なかったことがあった気がします。

# vue-unstatedを使うことのメリット
`vue-unstated` を使うことで得られるメリットがいくつかあるので触れておきます。

## 1. 自分でアノテーションしなくてもバッチリ型がつく
`provide/inject` を自分で叩いてストアを共有する場合、 `inject<HogeStore>(key)` のように**アノテーションでお型付けをする必要があります**。
しかし、unstatedコンテナでラップすると、 `Container.useContainer()` だけで**バッチリ型がついたStoreが返ってきます**。

## 2. 簡単にstoreの依存関係を表現できる
下記コード例のように、ストア同士の依存関係をシンプルに記述することができます[^3]。

```js
// 検索結果をfetchするだけのComposition
const useSearchResult = () => {
  // ...
  return { result, search }
}
export const SearchResultContainer = createContainer(useSearchResult)

// 検索結果をフィルタするだけのComposition
const useFilteredSearchResult = () => {
  // 検索結果をfetchするコンテナを利用する
  const { result } = SearchResultContainer.useContainer();
  // フィルタする処理をかける(実際はフィルタのパラメータの状態を持ったりしてもっと複雑になる)
  const filteredSearchResult = SomeFilterFunction(result);

  return { result: filteredSearchResult }
}
export const FilteredSearchResultContainer = createContainer(useFilteredSearchResult)
```

[^3]: まだ実際に動かして試してないので、動かなかったらメンゴです。動くように直します。

## 3. 利用しているコンポーネントツリーのインスタンスがGCされた場合、ストアもGCされる。
大規模なアプリケーションを運用していると、主にパフォーマンスの関係で、各ページをDynamic importしたり、いらない状態を消したかったりするのではないでしょうか。`vue-unstated`のメリットというより、`provide/inject`のメリットですが、**provideしたコンポーネントが消えれば参照カウントでunstatedインスタンスもGCされる**ため、**今表示されているコンポーネントツリーにだけ必要な状態を持つことが可能**です。

# まとめ
Vue Composition APIはまだまだこれからといったフェーズなので、nuxtのサーバサイドプロセスからのhydrationなど、Vuexから離れられなかったり、その他の状態管理方法にそれぞれの必要性や良さがあるのでないかと思っています。
ですが、今回ご紹介した `provide/inject` 方式は、いまの所使い勝手の良さからメインストリームになってもおかしくないと思っています。
これから事例を積み上げていってpros/consを精緻にしたり、さらなるベストプラクティスやバッドプラクティスを見つけていけたらなと思っています。
もしライブラリを使ってもらえた場合、気軽にフィードバックいただけるとありがたいです :pray: 

----

# 参考記事
- [Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか？](https://qiita.com/tmy/items/a545e44100247c364a71)
- [先取りVue 3.x !! Composition API を試してみる](https://qiita.com/ryo2132/items/f055679e9974dbc3f977)