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

Vue.observable & TypeScriptで手早く安心できる状態管理を手に入れる

More than 1 year has passed since last update.

Vue.jsにおける状態管理方法として何を使っているでしょうか。

  • dataにつっこむ
  • ReduxとかMobXとか

といった方法もありますが、もっともメジャーなものはVuexでしょう。

Vuex&TypeScriptがなかなか大変

フリーダムなフロントエンドに疲れたのか、最近はTypeScriptを採用するケースも増えているかと思います。型で保証されている安心感は一度味わうと抜けられないですよね。

しかし、実際やってみた方であれば実感するかと思いますが、VuexとTypeScriptを組み合わせるのはなかなかに大変です。
クラスコンポーネント化したVueコンポーネント内でのVuexとの接続に関しては、ktsnさんが公開されているvuex-classを利用することで可能となります(ありがたい)が、Vuex内部でのstate/getters/actions/mutationsの定義においてきっちり型で守ろうと思うと、独自で型定義を書く必要が生じるケースが多いです。

それでも不可能ではないので、頑張ってVuexのまま型で守るのも全然ありだと思いますが、Vuex&TypeScriptは考えなければいけないことが多くなりがちで、複雑ではないアプリケーションにおいてはコストが高く、スラスラコードを書けずに「少し重たいなぁ…」と感じやすいです。そこで...

Vue.observable & TypeScript

Vue.observable APIとTypeScriptを組み合わせることで、型で守られた状態管理をサクッと作ることができ、選択肢のひとつとして良さそうでした。

Vue.observable

Vue.js2.6で新たに追加されたAPIです。
https://jp.vuejs.org/v2/api/index.html#Vue-observable

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

つまり・・どういうことだ?となるかもしれませんが、これを利用することでVue.jsの算出プロパティ(computed)や描画関数(render)から変更を検知できるオブジェクトを外部に生成することが可能となります。

TypeScriptと組み合わせた簡単な状態管理例

名前と住所を保持した非常にシンプルな状態管理の例です
(コードは@vue/cliで作成したテンプレートをベースとしています。)

store.ts
import Vue from "vue";

export type State = {
  name: string;
  address: string;
}

export const state = Vue.observable<State>({
  name: "",
  address: ""
});
App.vue
<template>
  <div id="app">
    <div>
      name: <input type="text" :value="name" @input="updateName($event.target.value)" />
    </div>
    <div>
      address: <input type="text" :value="address" @input="updateAddress($event.target.value)" />
    </div>
    <hr />
    <div>{{ profile }}</div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { state } from "@/store/store";

@Component
export default class App extends Vue {
  get name() {
    return state.name;
  }
  get address() {
    return state.address;
  }
  get profile() {
    return `name:${this.name}/address:${this.address}`;
  }

  updateName(name: string) {
    state.name = name;
  }
  updateAddress(address: string) {
    state.address = address;
  }
}
</script>

注目すべきは、コンポーネント内でのstateの扱いです。
dataとして保持しているわけではなく、純粋に外部からimportしたものを直接computedから返却しています。

  get name() {
    return state.name;
  }

さらに、methodsではstateの値を直接更新しています。

  updateName(name: string) {
    state.name = name;
  }

非常にシンプルですね。
importしたstateの値を直接参照し、stateを直接書き換えているだけです。

これだけで、stateが変更されるとcomputedも再計算され、適切に再レンダリングが走るようになります。
(最初は「えっ、これどうやって動いてんの…?」と思いましたが、Vue.jsのコードを読むと仕組みはわかるので、興味のある方は見てみると良いかもしれません。)

image.gif

stateをコンポーネントから直接書き換えるのをやめる

規模感によっては全然このままでも良いのですが、好き放題stateを変えまくるというのはさすがにアレですので、変更もstore内で集約しておくとよいでしょう。

store.ts
...

export const mutations = {
  updateName(name: string) {
    state.name = name;
  },
  updateAddress(address: string) {
    state.address = address;
  }
};
App.vue
...

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { state, mutations } from "@/store/store";

@Component
export default class App extends Vue {
  get name() {
    return state.name;
  }
  get address() {
    return state.address;
  }
  get profile() {
    return `name:${this.name}/address:${this.address}`;
  }

  updateName(name: string) {
    mutations.updateName(name);
  }
  updateAddress(address: string) {
    mutations.updateAddress(address);
  }
}
</script>

VSCodeなどを使っていれば、サクッと型の不整合に気付くことができます。
image.png

Mixinにする

アクセス時に必要なcomputedをすべてMixin経由にしてしまうという手もあります。
これであればstateを公開する必要自体なくなるので、コンポーネントから好き放題やるのはある程度ガードできます。

store.ts
import { Vue, Component } from "vue-property-decorator";

type State = {
  name: string;
  address: string;
}

const state = Vue.observable<State>({
  name: "",
  address: ""
});

@Component
export default class StoreMixin extends Vue {
  get name() { return state.name; }
  get address() { return state.address; }

  updateName(name: string) {
    state.name = name;
  }
  updateAddress(address: string) {
    state.address = address;
  }
}
App.vue
...

<script lang="ts">
import { Component, Vue, Mixins } from "vue-property-decorator";
import StoreMixin from "@/store/store";

@Component
export default class App extends Mixins(StoreMixin) {
  get profile() {
    return `name:${this.name}/address:${this.address}`;
  }
}
</script>

これであれば、状態管理についてはMixin内部で閉じることができるので、テストも書きやすくなります。

Pros/Cons

Pros

Vue.js単体で状態管理を作れる

Vuexなどの外部ライブラリを必要としないのが大きいポイントだと思います。
Vue.js単体で完結するため、覚えることも少ないですし非常にシンプルです。

型定義をあんまり頑張らなくて済む

特に最終的なMixinの形にした場合、Stateの型定義をもとにほぼすべての箇所の型推論が効くようになります。
独自で頑張る必要がなく、少ない苦労で恩恵だけ大きく受けることができます。

(たぶん)理解しやすい

結局のところ、

  • ただのオブジェクトの値を参照
  • ただのオブジェクトの値を書き換え

という感覚で取り扱えるため、非常に直感的です。

Cons

オレオレ状態管理になる可能性がある

ほぼ純粋なオブジェクトの取扱いに近いため、実際にどう扱うかは、開発者の裁量に委ねられます。
さまざまな独自実装やユーティリティを作り込みすぎると複雑化していく恐れがあります。

やりすぎると「Vuexでよかったのでは...」となる

結局のところ、巨大な状態管理を取扱うようになると、モジュール分割や、処理の責務を切り分けたりといった工夫が必要になってくるでしょう。そうなると、結局「Vuexのようななにか」を独自で作るようなものになるため、逆にツラくなるかもしれません。

まとめ

個人的には、いきなりVuexを導入するのはコストが高いためできるだけ避けたほうが良いと思っています。

Vue.observableを利用することで小さい状態管理をシンプルに構築でき、かつTypeScriptと組み合わせることで、ストアの保護もある程度は静的に行うことが可能になり、かつ型によってリファクタリングなども容易になります。

このエントリーではSSRが必要なケースなどは想定していないので、どんなシチュエーションでも万能かと言われると微妙ですが、状態管理を構築する手段のひとつとしては有用なのではないかと思います。おわり。


宣伝
4/16の技術書典6で「jQuery to Vue.jsで学ぶ レガシーフロントエンド安全改善ガイド」という本を頒布します。
(Vue.observableも少し載ってます。)
https://techbookfest.org/event/tbf06/circle/40310003

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