Vue 2 + TypeScriptでVuexを使わないという選択肢

はじめに
--------------------------------

この記事はVueとTypeScriptの開発で状態管理について悩んでいる方へ，Vuexを使わないという選択をした場合どのように状態管理をしていくかを綴った記事です。
Vuexを否定することが目的ではありません，Vuexを使いたい場合は@tsrnkさんが書かれている[こちらの記事](https://qiita.com/tsrnk/items/fd95c3d8013d0795a260)のように[vuex-module-decorators](https://github.com/championswimmer/vuex-module-decorators)を使う方法をおすすめします。

Vue 3ではComposition APIに `ref`, `reactive` が追加されたのでこちらを使いましょう。

何に悩んでいたのか
--------------------------------

TypeScriptとVuexは相性が悪く型安全性について度々問題にぶつかります。
厳密にはTypeScriptで書くVueとVuexの相性が悪いです。
そもVue 2.xはJavaScript製であり，もともとTypeScriptのような型安全を考慮した作りにはなっていません。
ですのでTypeScriptの型定義ファイルによって型をあと付けして可能な限り型安全が保たれるようにしています。

Vuexで作ったストアをコンポーネントから呼び出すためにはコンポーネント内で`this.$store`を使います。
例えばストアの`count`というステートをコンポーネントから呼び出したい場合には以下の様に書きます。

```ts:store.ts
export default new Vuex.Store({
  state: {
    count: 0
  },
});
```

```ts:App.vue
@Component
export default class extends Vue {
  created() {
    console.log(this.$store.state.count);
  }
}
```

このとき`this.$store`の持つオブジェクトは`Store`型であり，ストアの内容である`StoreOptions`型のオブジェクトとは異なるものです。
これもVueがJavaScript製でありメタプログラミングによってVueとVuexのやり取りをしていることに起因しています。
そのため，コンポーネントからストアを呼び出す際には[vuex-class](https://github.com/ktsn/vuex-class)のようにVuexとコンポーネントを対応させるというアプローチでこの問題に対処してきました。

例えば[vuex-class](https://github.com/ktsn/vuex-class)ではコンポーネントを以下のように書きます(公式より抜粋)。

```vue
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
import { State, Getter, Action, Mutation, namespace } from 'vuex-class';

@Component
export class MyComp extends Vue {
  @State('foo') stateFoo;
  @State(state => state.bar) stateBar;
  @Getter('foo') getterFoo;
  @Action('foo') actionFoo;
  @Mutation('foo') mutationFoo;

  // If the argument is omitted, use the property name
  // for each state/getter/action/mutation type
  @State foo;
  @Getter bar;
  @Action baz;
  @Mutation qux;

  created() {
    this.stateFoo; // -> store.state.foo
    this.stateBar; // -> store.state.bar
    this.getterFoo; // -> store.getters.foo
    this.actionFoo({ value: true }); // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }); // -> store.commit('foo', { value: true })
    this.moduleGetterFoo; // -> store.getters['path/to/module/foo']
  }
}
</script>
```

以下はデコレーター部分だけ取り出したものです。
ストア内での名前を文字列で指定し，コンポーネント内で使いたい名前と対応させています。

```ts
  @State('foo') stateFoo;
  @State(state => state.bar) stateBar;
  @Getter('foo') getterFoo;
  @Action('foo') actionFoo;
  @Mutation('foo') mutationFoo;
```

このままでは対応させたステートやミューテーションは`any`のままなので，更にインタフェースを用いて型をあと付けすることができます。

しかしどうでしょうか，そも文字列内の名前を書き間違えてしまうと対応するものがなくなってしまうので動かなくなります。
型安全を手に入れるために文字列を使って安全ではない書き方をしなければならないという新たな危険をはらんでいるのです。
そして，Vuexが肥大化すればするほどVueとTypeScriptの型安全のために大量のインタフェースとデコレーターを書かなければなりません。

Vuexを使わないことのメリット
--------------------------------

### TypeScriptの知識のみでストアを書ける

Vue 2.6.0から登場した`Vue.observable()`により簡単にリアクティブなオブジェクトを作ることができるようになりました。
オブジェクトの内容はリアクティブ化する際に付与されるいくつかのプロパティやメソッドを除けば，それ以外は元のオブジェクトと同じです。
つまりは，ストアとして扱いたいオブジェクトと`import`と`export`を書ければ良いのでTypeScriptの知識のみで実装することができ追加のコストがかかりません。

### 大量の型をあと付けしなくて良い

先に挙げた大量のデコレーターを書く必要がなくなり，`import`と`export`を使ってオブジェクトを引き回しても型安全が保たれるのでコードの記述量が減ります。

### ファイルサイズが減る

ごくわずか(10KB程度)とはいえVuexを含めなくて良い分ファイルサイズが減ります。

Vuexを使わないことのデメリット
--------------------------------

### 公式の開発ツール拡張と連携できない

Vue公式による[開発ツール拡張](https://github.com/vuejs/vue-devtools)との連携ができなくなるので，タイムトラベルデバッグやスナップショットを利用できなくなります。
これはVuexを使わないことの大きなデメリットになります。
Vuexの力が必要な規模の開発では[vuex-module-decorators](https://github.com/championswimmer/vuex-module-decorators)を検討しましょう。

ストアの実装
--------------------------------

ストアを作る前に`Vue.observable()`について知っておきます。
公式より以下のように書かれています。

>オブジェクトをリアクティブにします。内部的には、Vue は data 関数から返されたオブジェクトに対してこれを使っています。

>戻り値のオブジェクトは、描画関数 や 算出プロパティ の中で直接使え、値が変更されたときには適切な更新をトリガーします。単純なシナリオでは、コンポーネントをまたぐ最小の state ストアとして使用することもできます:

つまり，`Vue.observable()`にオブジェクトを渡すとリアクティブなオブジェクトにしてくれるというもの。
例えば以下のようなオブジェクトを作ります。

```ts
const store = {
  count: 0
};
```

なんの変哲もないただのオブジェクトですが，`store`を`console.log()`等で出力して確認してみます。

![78117a9a-9cd7-30b6-47da-b19e482deffe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128213/78117a9a-9cd7-30b6-47da-b19e482deffe.png)

これを`Vue.observable()`に渡してみます。

```ts
import Vue from 'vue';

const store = Vue.observable({
  count: 0
});
```

すると以下のようになります。

![01c54faa-a682-5585-47c9-2587fb798f3d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128213/01c54faa-a682-5585-47c9-2587fb798f3d.png)

`Observer`オブジェクトの入った`__ob__`プロパティと`count`のゲッターメソッドとセッターメソッドが生えました。
`count`プロパティへのアクセスはこのゲッターメソッドとセッターメソッドを経由することでリアクティブを実現しているわけですね。

注意ですが公式で次のように書かれています。

>Vue 2.x では、Vue.observable は渡されたオブジェクトを直接操作するため、ここでデモされる ように戻り値のオブジェクトと等しくなります。Vue 3.x では、代わりにリアクティブプロキシを返し、元のオブジェクトを直接変更してもリアクティブにならないようにします。そのため、将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨します。

つまり，参照によって引数に与えられたオブジェクトをリアクティブにするので次のようにも書けるわけですね。

```ts
const store = {
  count: 0
};

Vue.observable(store);
```

しかし，Vue 3からは元のオブジェクトが変わらないようになるとのことなので，戻り値から受け取ったほうが良さそうです。

さて，このままストアとして利用しても良いですがクラスで書き直します。
クラスで書くことで継承やカプセル化をより簡単に書けるようになります。

一旦`Vue.observable()`は使わず以下のようにクラスに書き直しインスタンス化します。

```ts:store.ts
class Store {
  private count = 0;
}

const store = new Store();
```

![f16c0830-8cca-c425-9cf8-5be5e3393503.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128213/f16c0830-8cca-c425-9cf8-5be5e3393503.png)

`Store`クラスからインスタンス化したので`Store`オブジェクトができました。
この`Store`オブジェクトもオブジェクトですので`Vue.observable()`でリアクティブにできるはずです。
以下のようにリアクティブ化します。

```ts:store.ts
import Vue from 'vue';

class Store {
  private count = 0;
}

const store = Vue.observable(new Store());
```

![fd7013b7-ac4e-b561-0322-ab09167383e0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128213/fd7013b7-ac4e-b561-0322-ab09167383e0.png)

先程と同様にリアクティブに必要な情報が付与されました。

続けてVuexのゲッターやミューテーションと同様のものをクラスに実装します。
プロパティが`_count`になっている点に注意してください。
また，他所で`import`するために`export default`しています。

```ts:store.ts
import Vue from 'vue';

class Store {
  private _count = 0;

  public get count() {
    return this._count;
  }

  public increment() {
    this._count++;
  }

  public decrement() {
    this._count--;
  }
}

export default Vue.observable(new Store());
```

`private`修飾子によって`_count`は隠蔽され，プロパティの変更は`increment()`と`decrement()`ミューテーションからのみ操作できるようになりました。

次にコンポーネントからストアを呼び出してみます。

```vue:App.vue
<script lang="ts">
import store from '@/store';
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class extends Vue {
  private store = store;

  private onPlusClicked() {
    store.increment();
  }

  private onMinusClicked() {
    store.decrement();
  }
}
</script>

<template>
  <div>
    <p>{{ store.count }}</p>
    <button @click="onPlusClicked" type="button">+</button>
    <button @click="onMinusClicked" type="button">-</button>
  </div>
</template>
```

さいごに
--------------------------------

[公式](https://vuex.vuejs.org/ja/#%E3%81%84%E3%81%A4%E3%80%81vuex%E3%82%92%E4%BD%BF%E3%81%86%E3%81%B9%E3%81%8D%E3%81%A7%E3%81%97%E3%82%87%E3%81%86%E3%81%8B%EF%BC%9F)にあるようにシンプルなストアパターンで良い場合は，この方法であれば安全に低コストで実装することができます。
また，ある程度の規模であっても集中型のストアかつ型安全であるが故に，状態やコンテキストの把握が容易になるので[開発ツール拡張](https://github.com/vuejs/vue-devtools)の補助を必要としない場合にも良い方法だと思います。