Help us understand the problem. What is going on with this article?

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

はじめに

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

何に悩んでいたのか

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

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

store.ts
export default new Vuex.Store({
  state: {
    count: 0
  },
});
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のようにVuexとコンポーネントを対応させるというアプローチでこの問題に対処してきました。

例えばvuex-classではコンポーネントを以下のように書きます(公式より抜粋)。

<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>

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

  @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()により簡単にリアクティブなオブジェクトを作ることができるようになりました。
オブジェクトの内容はリアクティブ化する際に付与されるいくつかのプロパティやメソッドを除けば,それ以外は元のオブジェクトと同じです。
つまりは,ストアとして扱いたいオブジェクトとimportexportを書ければ良いのでTypeScriptの知識のみで実装することができ追加のコストがかかりません。

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

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

ファイルサイズが減る

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

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

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

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

ストアの実装

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

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

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

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

const store = {
  count: 0
};

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

78117a9a-9cd7-30b6-47da-b19e482deffe.png

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

import Vue from 'vue';

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

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

01c54faa-a682-5585-47c9-2587fb798f3d.png

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

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

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

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

const store = {
  count: 0
};

Vue.observable(store);

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

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

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

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

const store = new Store();

f16c0830-8cca-c425-9cf8-5be5e3393503.png

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

store.ts
import Vue from 'vue';

class Store {
  private count = 0;
}

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

fd7013b7-ac4e-b561-0322-ab09167383e0.png

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

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

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()ミューテーションからのみ操作できるようになりました。

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

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>

さいごに

公式にあるようにシンプルなストアパターンで良い場合は,この方法であれば安全に低コストで実装することができます。
また,ある程度の規模であっても集中型のストアかつ型安全であるが故に,状態やコンテキストの把握が容易になるので開発ツール拡張の補助を必要としない場合にも良い方法だと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした