【Vue.js, Nuxt.js】storeを使用してTypeScript側からsnackbarを起動させる

# この記事について
以下のようなsnackbarをVueのtemplate上での `v-if` や `v-show` での表示制御ではなく、TypeScript側から任意のタイミングで表示できるようにする方法をこの記事で紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234275/6277e7e8-7306-5ff4-42d1-6261dda39c97.png)

# 対象読者
* Vue.js（やNuxt.js）でsnackbarをTypeScript側から表示したい方
* Vue.jsのstoreの実装を行ったことのある方
* Vuetifyの（簡易的な）実装を行ったことのある方

# 環境
|内容|バージョン|
|---|---|
|nuxt|2.15.8|
|nuxt-typed-vuex|0.3.0|
|vue|2.6.14|
|vuetify|2.6.1|

以下の内容は、Nuxt.jsを使用していることを前提とします。
ディレクトリ構成

```
sampleProject
├componens
| └snackbarError.vue
├pages
| └index.vue
├store
| ├index.ts
| └snackbarError.ts
└types
  └index.d.ts
```

# 処理の流れ
考え方としては、「スナックバー用のstoreを作成して、storeの値変更時にスナックバーコンポーネントを表示する」として、「スナックバーコンポーネントをグローバルに扱う」考え方としております。

スナックバーを表示したい処理の実行
↓
スナックバーstoreのメッセージデータ更新
↓
スナックバーstoreのメッセージデータ更新をトリガにスナックバーコンポーネントを表示

# スナックバー用のstoreの作成
storeはスナックバー用の個別storeを管理する `snackbarError.ts` と 他のstoreも含めて管理する `index.ts` に分けております。

```store/snackbarError.ts
import { mutationTree, actionTree } from 'typed-vuex';

// スナックバーコンポーネント上に表示するメッセージのデータをstoreに保持する
export const state = () => ({
  message: '' as string,
});
export type RootState = ReturnType<typeof state>;

export const mutations = mutationTree(state, {
  open(state, message: string) {
    state.message = message;
  },
});

export const actions = actionTree(
  { state, mutations },
  {
    open({ commit }, message: string) {
      commit('open', message);
    },
  },
);
```

```store/index.ts
import {
  getAccessorType,
  getterTree,
  mutationTree,
  actionTree,
} from 'typed-vuex';
// その他のstoreがある場合はここで他のstoreをimportする
import * as snackbarError from './snackbarError';

export const state = () => ({});
export const getters = getterTree(state, {});
export const mutations = mutationTree(state, {});
export const actions = actionTree({ state, getters, mutations }, {});
export const accessorType = getAccessorType({
  state,
  getters,
  mutations,
  actions,
  modules: {
    // その他のstoreがある場合はmodulesに他のstoreを追加する
    snackbarError,
  },
});
```

VueファイルのTypeScriptからstoreにアクセスできるように、`index.d.ts` を以下のように編集します。

```types/index.d.ts
import { accessorType } from '~~/store';

declare module 'vue/types/vue' {
  interface Vue {
    $accessor: typeof accessorType;
  }
}

declare module '@nuxt/types' {
  interface NuxtAppOptions {
    $accessor: typeof accessorType;
  }
}
```

# スナックバーコンポーネントの作成
先ほど作成したスナックバーstoreのstate変更を検知するように実装（ `this.$store.subscribe` の部分）します。
スナックバーは[Vuetifyのコンポーネント](https://vuetifyjs.com/ja/components/snackbars/)を使用しております。

```componens/snackbarError.vue
<template>
  <v-snackbar v-model="isShown" top max-width="60%" color="error">
    <div class="pre-wrap" v-text="message" />

    <template #action="{ attrs }">
      <v-btn
        small
        plain
        shaped
        multi-line
        v-bind="attrs"
        @click="onClickedCloseButton"
      >
        <v-icon> mdi-close </v-icon>
      </v-btn>
    </template>
  </v-snackbar>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  name: 'SnackbarError',
  data() {
    return {
      isShown: false,
      message: '',
    };
  },
  created() {
    // storeの値変更を検知して、storeに送られたメッセージを画面上に表示する
    this.$store.subscribe((mutation, state) => {
      if (mutation.type === 'snackbarError/open') {
        const message = state.snackbarError.message;
        if (message === null || message === '') {
          return;
        }

        this.message = message;
        this.isShown = true;
      }
    });
  },
  methods: {
    onClickedCloseButton(): void {
      this.isShown = false;
    },
  },
});
</script>

<style lang="sass" scoped>
// 改行コードはそのまま改行して表示
.pre-wrap
  white-space: pre-wrap
</style>
```

# TypeScriptからスナックバーを表示
スナックバーを表示する際は、`components/snackbarError.vue` を直接呼び出すのではなく、先ほど作成したstoreにメッセージを送ってstore経由でスナックバーを表示します。
vueのtemplate内でsnackbarを `v-if` や `v-show` で表示制御せずともTypeScriptから表示することができるようになりました。

```pages/index.vue
<template>
  <button @click="showError">エラー表示</button>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  methods: {
    showError() {
      this.$accessor.snackbarError.open('エラーが発生しました。');
    },
  },
});
</script>
```

Nuxt.jsの場合は、pluginsやmiddlewareなどTypeScriptの `Context` 型にアクセスできる箇所であれば以下のように記述することでsnackbarを起動することができるようになります。

```types/index.d.ts
import { accessorType } from '@f/store';

declare module 'vue/types/vue' {
  interface Vue {
    $accessor: typeof accessorType;
  }
}

declare module '@nuxt/types' {
  interface NuxtAppOptions {
    $accessor: typeof accessorType;
  }
  // Context型からもstoreへアクセスできるように型定義を追加する
  interface Context {
    $accessor: typeof accessorType;
  }
}
```

```ts
import { Context, Plugin } from '@nuxt/types';

const plugin: Plugin = ({ $accessor }: Context) => {
  $accessor.snackbarError.open('エラーが発生しました。');
};

export default plugin;
```

# 参考
[How to create a global snackbar using Nuxt, Vuetify and Vuex.](https://dev.to/stephann/how-to-create-a-global-snackbar-using-nuxt-vuetify-and-vuex-1bda)