🐈

Vue2+Typed Vuexでの型推論

2021/11/01に公開

はじめに

はじめてのToDoアプリ開発の続きです。Vue2+Vuexでもdispatchで型推論ができるTyped Vuexを導入すると、どのような実装になるかをご紹介します。

Vuexについて

Vuexは、複数ページ間のデータ保存場所として利用します。よく、画面階層が深いときに、$emitやpropsを使わなくて便利との記事も見かけますが、Composition APIのreactiveで操作可能ですし、Vuexはグローバル変数になるため、利用は必要最低限にとどめたほうがいいと思います。

あとは、Vuexで使われる用語について、簡単に説明します。
Vuexイメージ

  • state ... データ構造を定義する。
  • mutations ... データを同期で更新する。Vuex5で廃止予定。
  • actions ... データを非同期で更新する。(画面からはここを利用する)
  • getters ... データを参照・算出する。computedと同じ。共通化するときはgetterにする。
  • modules ... データストアを分割する。

Typed Vuexでの書き方

Vuexはコンポーネントでの呼び出しでTypescriptの型推論ができず、色々な対策が記事にされています。ただ、よく見かけるClassStyle方式はVue3以降に非推奨になるため、今回はTyped Vuexを採用しました。Composition APIでは、SetupContextの$rootが参照できなくなったので、どう使えばいいのか苦労しました。

まずは、Vuex, Typed Vuexのパッケージを追加します。

> npm i vuex typed-vuex --save

次に、tasksをstoreでデータ管理するように変更します。以前のサンプルでは、各コンポーネントにロジックが書かれていましたが、storeで一元管理するようにします。
基本的な書き方は、Vuex4のCompositionAPIのサンプルとほぼ同じです。

store/modules/tasks.ts
import { mutationTree, actionTree, getterTree } from "typed-vuex";

// 型定義を移動
export interface Task {
  id: number;
  title: string;
  done: boolean;
}

// 型定義を追加しないとeslintでエラーになる。
interface TaskState {
  tasks: Task[];
}

// state, getters, mutations, actionsを追加
export const state = (): TaskState => ({
  tasks: [],
});

export const getters = getterTree(state, {
  get: (state) => state.tasks,
  getById: (state) => (id: number) => state.tasks.find((t) => t.id === id),
});

export const mutations = mutationTree(state, {
  set(state, tasks: Task[]) {
    state.tasks = tasks;
  },
  done(state, id: number) {
    const task = state.tasks.find((t) => t.id === id);
    if (task !== undefined) {
      task.done = !task.done;
    }
  },
  add: (state, task: Task) => {
    state.tasks.push(task);
  },
  remove: (state, id: number) => {
    state.tasks = state.tasks.filter((t) => t.id !== id);
  },
});

export const actions = actionTree(
  { state, mutations },
  {
    async set({ commit }, tasks: Task[]) {
      commit("set", tasks);
    },
    async done({ commit }, id: number) {
      commit("done", id);
    },
    async add({ commit }, title: string) {
      commit("add", {
        id: Date.now(),
        title: title,
        done: false,
      });
    },
    async remove({ commit }, id: number) {
      commit("remove", id);
    },
  }
);
store/index.ts
import Vue from "vue";
import Vuex from "vuex";
import { useAccessor } from "typed-vuex";
// exportされた定義をすべてtasksとして読み込み
import * as tasks from "./modules/tasks";

// Vuexを使えるようにする。
Vue.use(Vuex);

// tasksをmodulesで登録、modulesの中にmodulesを定義することも可能。
const storePattern = {
  modules: {
    tasks,
  },
};

// accessor経由でstore参照が可能になる。
export const accessor = useAccessor(new Vuex.Store(storePattern), storePattern);

最後に、利用する側も修正していきます。

views/Home.vue
<template>
  <div>
    <!-- 引数なしに変更 -->
    <TodoAdd></TodoAdd>
    <TodoList></TodoList>
  </div>
</template>

<script lang="ts">
import TodoList from "@/components/TodoList.vue";
import TodoAdd from "@/components/TodoAdd.vue";
import { defineComponent } from "@vue/composition-api";
import { accessor } from "@/store";

export default defineComponent({
  name: "Home",
  components: { TodoList, TodoAdd },
  setup() {
    // accessorを使って、データをセットする。
    accessor.tasks.set([
      {
        id: 1,
        title: "起きる",
        done: false,
      },
      {
        id: 2,
        title: "着替える",
        done: false,
      },
    ]);
  },
});
</script>
components/TodoAdd.vue
<script lang="ts">
import { ref, defineComponent } from "@vue/composition-api";
// accessorを追加
import { accessor } from "@/store";

export default defineComponent({
  name: "TaskList",
  setup() {
    const newTaskTitle = ref("");
    const addTask = () => {
      // accessor経由でtasksを追加する。
      accessor.tasks.add(newTaskTitle.value);
      newTaskTitle.value = "";
    };

    return { newTaskTitle, addTask };
  },
});
</script>
components/TodoAdd.vue
import { computed, defineComponent } from "@vue/composition-api";
// accessorを追加
import { accessor } from "@/store";

export default defineComponent({
  name: "TaskList",
  setup() {
    // accessor経由でtasksを取得、更新、削除する。
    // computedを入れないと、表示が更新されない。
    const tasks = computed(() => accessor.tasks.get);
    // const tasks = computed(() => accessor.tasks.tasks);
    const doneTask = (id: number) => accessor.tasks.done(id);
    const deleteTask = (id: number) => accessor.tasks.remove(id);

    return { tasks, doneTask, deleteTask };
  },
});
</script>

おわりに

タスクの更新ロジックがstoreで一元管理され、propsの実装やTaskの型定義の読み込みが不要になりました。また、Vuex単体での利用に比べて、型推論ができるように、タイプミスが減らせると思います。

Discussion