Vuexのstateをcomposition-apiでReactiveに使う

Vue.js版のReact HooksにあたるComposition APIとVuexを組み合わせる方法を紹介します。

> vuejs/composition-api: Vue2 plugin for the Composition API.
> https://github.com/vuejs/composition-api

こちらの方の投稿でVuexのstateそのものをreactiveにする方法を紹介されていますが、今回はVuexはそのままにしておく方法です。

> Nuxt.js + Composition APIでVuexのStateをReactiveに使う方法
> https://qiita.com/tubone/items/f5c7e8e79e21b051eec4

## Composition APIによるカウンターの実装

単純なカウンターを例にComposition APIで実装すると以下のようになるかと思います。

```ts
export default createComponent({
  setup() {
    const state = reactive({
      count: 0,
    })
    return {
      state,
      handleIncrementButtonClick() {
        state.count++
      }
    }
  }
})
```

これを使って説明を進めていきます。

## Vuex Storeのwatch関数を使って変更を監視する

さてVuex Storeはそれ自体でリアクティブな仕組みを持っていますが、Composition API（というかVueコンポーネントそのもの）とは別のライフサイクルなのでそれぞれが勝手に連携することはありません。

そこでStoreのwatch関数を使うことで対象の変更を監視することができます。
(Vuex側にも同じcountというstateが存在しそれを更新するactionがあることにします)

```ts
export default createComponent({
  setup(props: Props, context: SetupContext) {
    const state = reactive({
      count: 0,
    });

    // setup関数内はcreatedフックと同じタイミングで動く
    const unwatch = context.root.$store.watch<number>(vuexState => {
      // ここでreturnした値が監視対象になる
      return vuexState.count;
    }, (newVal: number) => {
      // Vuexのstateが更新されるとこの関数が呼ばれるのでreactiveの値にセットして通知
      state.count = newVal;
    });

    // コンポーネントが消されるときに監視を止める
    onUnmounted(() => {
      unwatch();
    });

    return {
      state,
      handleIncrementButtonClick() {
        // ここは普通にactionの呼び出し
        context.root.$store.dispatch('incrementCount');
      }
    }
  }
});
```

## Storeの参照と監視を一般化する

このままでは使い勝手が悪いので一般化した関数にして楽に取れるようにします。
一旦これでnamespacedでないstateであれば参照が可能です。
contextを引数に渡さないようにできないもんですかね。

```ts
function vuexStateRef<T>(context: SetupContext, key: string): Ref<T> {
  // 最後に as Ref<T> をしておかないと data.value の更新の部分で型エラーが起きました
  const data = ref<T>(context.root.$store.state[key] as T) as Ref<T>;

  const unwatch = context.root.$store.watch<T>((vuexState: any) => {
    // 階層化された値の監視には対応してない
    return vuexState[key] as T;
  }, (newVal: T) => {
    // refのvalueを更新すればコンポーネントが反応する
    data.value = newVal;
  });

  onUnmounted(() => {
    unwatch();
  });

  // Refを返す
  return data;
}
```

setup関数で使うときはこうします。

```ts
export default createComponent({
  setup(props: Props, context: SetupContext) {
    const state = reactive({
      count: vuexStateRef<number>(context, 'count'),
    });

    return {
      state,
      handleIncrementButtonClick() {
        context.root.$store.dispatch('incrementCount')
      }
    };
  }
})
```

かなり見通しが良いですね。

## 追記2020/06/05

stateを取り出す時、文字列で指定すると型引数も必要になったりnamespacedなやつに対応できないので、stateを取り出す関数を渡してやるといい感じになります。
また同時にgetterにも対応できるようになるので関数名は `useVuex` に変えました。

```ts
function useVuex<T>(context: SetupContext, getState: () => T): Ref<T> {
  // 最後に as Ref<T> をしておかないと data.value の更新の部分で型エラーが起きました
  const data = ref<T>(getState()) as Ref<T>;

  const unwatch = context.root.$store.watch<T>(
    // 
    getState,
    (newVal: T) => {
      // refのvalueを更新すればコンポーネントが反応する
      data.value = newVal;
    }
  );

  onUnmounted(() => {
    unwatch();
  });

  // Refを返す
  return data;
}

export default defineComponent({
  setup() {
    const state = reactive({
      count: useVuex<number>(() => store.state.count),
      doubleCount: useVuex<number>(() => store.getters['doubleCount'])
    })
    return {
      state,
      increment() {
        store.dispatch('increment')
      }
    }
  }
})
```

## まとめ

そのうち公式からVuexとの連携部分もリリースされるかと思いますが、Composition APIの勉強ついでに自作してみました。
actionsの連携部分で型定義も勝手に引き継げるようになるとかなりいいんですが、この辺りのアップデートに期待ですね。