LoginSignup
23
21

More than 3 years have passed since last update.

nuxt + typescript + vuex + axios に手を焼いたので共有

Last updated at Posted at 2019-12-12

こんにちは
nuxt + typescript でフロントエンドを作っているエンジニアです。
store の getter や actions にも type を効かせるときに手を焼いたので共有します。

目次

ベース作成

nuxt × typescript の構築は他の記事に譲るとし、今回の説明で必要なファイルを列挙します。
なお、今回は shops を中心に store の説明をします。

まずは型定義

types/index.d.ts
export interface Shop {
  name: string;
}

そして pages

pages/index.vue
<template>
  <div>
    {{ shopOptions }}
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
import { mapGetters } from 'vuex';
import { Shop } from '~/types';

export default Vue.extend({
  computed: {
    ...mapGetters({
      shopOptions: 'shops/values',
    }),
  },
  mounted() {
    this.$store.dispatch('shops/fetch');
  },
});
</script>

今回問題となっている store
fetch を実行すると、サーバーからファイルを取得し、values に保持します。
clear を実行すると、保持していた values を clear します。

store/shops.ts
import { Shop } from "~/types";
import { GetterTree } from "vuex/types/index";
import { RootState } from "~/store/types";

export const types = {
  SET: 'SET',
  CLEAR: 'CLEAR',
};

interface State {
  values: Shop[];
}

export const state: () => State = () => ({
  values: [],
});

export const mutations = {
  [types.SET](state: State, shops: Shop[]): void {
    state.values = shops;
  },
  [types.CLEAR](state: State): void {
    state.values = [];
  },
};

export const actions = {
  async fetch({ commit, getters }): Promise<void> {
    if (getters.values.length > 0) {
      return;
    }
    const { data } = await this.$axios.get('/shops');
    commit(types.SET, data);
  },

  clear({ commit }): void {
    commit(types.CLEAR);
  },
};

export const getters: GetterTree<State, RootState> = {
  values(state: State): Shop[] {
    return state.values;
  }
};

以上の 3ファイルになります。

上記のコードだと、 component 側で typo しても、なんのエラーも出ません。

pages/index.vue
  mounted() {
      this.$store.dispatch('shops/fech'); // typo
  },
</script>

これを typescript のエラーとして警告してくれるのがゴールです。

公式の見解

Nuxt TypeScript では、以下の選択肢が与えられています

  • vanilla (今回は触れません)
  • vuex-module-decorators
  • vuex-class-component

ググってみると、vuex-module-decorators がベストのような記事がありますが、
vuex-class-component もいいよ!みたいな記事もあったので、試してみました。

vuex-module-decorators で型付け

まずは vuex-module-decorators で型付けをするやり方を見ていきます。

いつも通りライブラリをインストールします。

yarn add vuex-module-decorators

store を直していきます。
axios は後ほどやるので、ひとまずは仮データで進めます。

store/shops.ts
import {
  Module,
  VuexModule,
  Mutation,
  Action,
} from "vuex-module-decorators";
import { Shop } from "~/types";

@Module({
  name: 'shops',
  stateFactory: true,
  namespaced: true,
})
export default class ShopsStore extends VuexModule {
  private shops: Shop[] = [];

  @Mutation
  private SET(shops: Shop[]): void {
    this.shops = shops;
  }

  @Mutation
  private CLEAR(): void {
    this.shops = [];
  }

  @Action({})
  public async fetch(): Promise<void> {
    if (this.shops.length > 0) {
      return;
    }
    const data = [{ name: 'tokyo' }];
    this.SET(data);
  }

  @Action({})
  public clear(): void {
    this.CLEAR();
  }

  public get values(): Shop[] {
    return this.shops;
  }
}

State は内部変数として書きます。private をつけると直接参照したときに Typescript エラーを起こしてくれます。
getter は public get のメソッドとして書き直します。

次に Initialize plugin を設定していきます。公式で書かれている内容なのでさらっといきます。

store/index.ts
import { Store } from 'vuex';
import { initializeStores } from '~/utils/store-accessor';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const initializer = (store: Store<any>): void => initializeStores(store);

export const plugins = [initializer];
export * from '~/utils/store-accessor';
utils/store-accessor.ts
import { Store } from 'vuex';
import { getModule } from 'vuex-module-decorators';
import ShopsStore from '~/store/shops';

let shopsStore: ShopsStore;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function initializeStores(store: Store<any>): void {
  shopsStore = getModule(ShopsStore, store);
}

export { initializeStores, shopsStore };

設定した store を読み込むように pages 側を変更します。

pages/index.vue
<script lang="ts">
+ import { shopsStore } from '~/store';

(中略)

   computed: {
-    ...mapGetters({
-      shopOptions: 'shops/values',
-    }),

+   shopOptions(): Shop[] {
+     return shopsStore.values;
+   },
  },

  mounted() {
-   this.$store.dispatch('shops/fetch');
+   shopsStore.fetch();
  },
</script>

ここまでの変更で、typescript error が出るようになります。

pages/index.vue
<script lang="ts">
(中略)

  mounted() {
    shopsStore.fech(); // typo
  },
</script>
180:24 Property 'fech' does not exist on type 'ShopsStore'. Did you mean 'fetch'?
  > 180 |       await shopsStore.fech();
        |                        ^

以上で vuex-module-decorators による型付けは成功しました。
axios がまだ設定できていないので、そちらはおまけでやっていきます。

vuex-class-component で型付け

vuex-class-component で型付けをするやり方を見ていきます。

ライブラリをインストールします。

yarn add vuex-class-component

store を直していきます。

store/shops.ts
import {
  createModule,
  mutation,
  action,
  createProxy,
  extractVuexModule,
} from "vuex-class-component";
import Vuex from 'vuex';
import Vue from 'vue';
import { $axios } from '~/utils/api';
import { Shop } from "~/types";

Vue.use(Vuex);

const VuexModule = createModule({
  namespaced: 'shops',
  strict: false,
  target: 'nuxt',
});

export class ShopsStore extends VuexModule {
  private shops: Shop[] = [];

  @mutation
  private SET(shops: Shop[]): void {
    this.shops = shops;
  }

  @mutation
  private CLEAR(): void {
    this.shops = [];
  }

  @action
  public async fetch(): Promise<void> {
    if (this.shops.length > 0) {
      return;
    }
    const data = [{ name: 'tokyo' }];
    this.SET(data);
  }

  @action
  public async clear(): Promise<void> {
    this.CLEAR();
  }

  public get values(): Shop[] {
    return this.shops;
  }
}

const store = new Vuex.Store({
  modules: {
    ...extractVuexModule(ShopsStore),
  }
});

export const shopsStore: ShopsStore = createProxy(store, ShopsStore);

vuex-module-decorators と似ていますが、Module 指定のや store 登録周りの書き方が違います。
また、vuex-module-decorators とは違い、initialize plugin にあたる処理を各 store の中でやっています。

設定した store を読み込むように pages 側を変更します。

pages/index.vue
<script lang="ts">
import { shopsStore } from '~/store/shops';

(中略)

   computed: {
-    ...mapGetters({
-      shopOptions: 'shops/values',
-    }),

+   shopOptions(): Shop[] {
+     return shopsStore.values;
+   },
  },

  mounted() {
-   this.$store.dispatch('shops/fetch');
+   shopsStore.fetch();
  },
</script>

vuex-module-decorators の時と同じように、typescript error が出るようになります。

pages/index.vue
<script lang="ts">
(中略)

  mounted() {
    shopsStore.fech(); // typo
  },
</script>
180:24 Property 'fech' does not exist on type 'ShopsStore'. Did you mean 'fetch'?
  > 180 |       await shopsStore.fech();
        |                        ^

vuex-class-component での注意点

pages/index.vue
shopsStore.CreateProxy(~, ~)

このような形で、store.CreateProxy() を呼びましょうと書いてある記事が多かったのですが、これは古い readme に書かれていた内容のようです。(2019/12/12 時点)

まとめ

調べてみる限り vuex-module-decorators が優勢のようですが、書いてみると vuex-class-component の方が変更ファイルも少なく、書きやすい印象がありました。
公式は vuex-module-decorators を推してるけど、vuex-class-component も悪くないよ! という内容でした。

ご指摘あったらコメントください!

axios の設定

上記のコードでは axios の設定ができていないので、やっていきます。
とはいえ、公式 にも書いてあるので、参考程度に載せておきます。
なお、vuex-module-decorators でも vuex-class-component でも同じ書き方で動きます。

utils/api.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios';

let $axios: NuxtAxiosInstance;

export function initializeAxios(axiosInstance: NuxtAxiosInstance): void {
  $axios = axiosInstance;
}

export { $axios };
plugins/axios-accessor.ts
import { Plugin } from '@nuxt/types';
import { initializeAxios } from '~/utils/api';

export const accessor: Plugin = ({ $axios }): void => {
  initializeAxios($axios);
};

export default accessor;
nuxt.config.js
  plugins: [
    '@/plugins/axios-accessor',
  ]
store/shops.ts
import { $axios } from '~/utils/api';

(中略)

    const { data } = await $axios.get('/shops');

以上!

23
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
21