Vue/Vuetify Vuexを使ってみる

Vue.jsで親子関係でない独立したコンポーネント間でもデータ共有したい場合は状態管理ライブラリVuexが便利です。Vuexの基本的な設置方法や注意点、別コンポーネントとの同期方法をVuetifyでサンプルを作って確認します。

開発環境

Laradock v10
Laravel 7
Vue 2.6
Vuetify 2.2

Vuexのインストール

# npm install vuex

ファイル構成

js
 ┣ components
 ┃ ┣ HeaderComponent.vue
 ┃ ┣ Vuex1Component.vue
 ┃ ┗ Vuex2Component.vue
 ┣ store
 ┃ ┣ index.js
 ┃ ┗ user.js
 ┣ app.js
 ┗ router.js

js/store/user.js

user状態管理のモジュールです。

// user情報(状態)
const state = {
  user: {},
}
// state(user情報)を返す
const getters = {
  user: (state) => state.user,
}
// 同期で状態を変更する(直接stateを変更する)
const mutations = {
  setUser(state, user) {
    state.user = user;
  },
}
// 同期/非同期で状態を変更(mutationsを経由してstateを変更する)
const actions = {
  setUser(context, user) {
    context.commit('setUser', user);
  }
}

export default {
  namespaced: true,	// 名前空間を有効にする
  state,
  getters,
  mutations,
  actions
}

※Axiosなど非同期でデータを取得して状態を変化させる場合はactionsにメソッドを書くことになります。
※モジュールが増えてきたときにメソッド名等が被ってくる可能性を考慮してnamespaced: trueにしておきましょう。

js/store/index.js

Vuexもモジュールを設定しapp.jsに読み込ませるファイルですね。
もちろん、複数設定できます。

import Vue from 'vue';
import Vuex from 'vuex';
import user from './user';
 
Vue.use(Vuex);
 
export default new Vuex.Store({
  modules: {
    user: user,
  }
});

js/app.js

js/store/index.jsのインポートし、Vueインスタンスにstoreオプションを追加します。

require('./bootstrap');

window.Vue = require('vue');

import router from './router';
import store from './store/index';	// 追加
import Vuetify from 'vuetify';
import 'vuetify/dist/vuetify.min.css';
Vue.use(Vuetify);

Vue.component('example-component', require('./components/ExampleComponent.vue').default);
Vue.component('header-component', require('./components/HeaderComponent.vue').default);

const app = new Vue({
    el: '#app',
    router,
    store,	// 追加
    vuetify: new Vuetify()
});

userの状態を変更・取得

名前空間を有効にしてjs/store/index.jsのモジュール設定で

  modules: {
    user: user,
  }

としているので”user/〇〇”でアクセスすることになります。

  modules: {
    info: user,
  }

と変更すれば”info/〇〇”になりますね。

ミューテーションで変更

アクションでも使用しているcommitメソッドで変更します。

this.$store.commit("user/setUser", this.user)

アクションで変更

dispatchメソッドで変更します。
結果的にミューテンションを介してstateを変更することになりますが非同期で状態を変化させる場合はactionsで変更します。

this.$store.dispatch("user/setUser", this.user)

取得

this.$store.getters["user/user"]

namespaced: falseとした場合

js/store/user.jsで名前空間の設定をデフォルト(namespaced: false)にした場合は書き方が変わりますので注意が必要です。
それぞれ以下のようになります。

this.$store.commit("setUser", this.user)
this.$store.dispatch("setUser", this.user)
this.$store.getters.user

使ってみる

router.jsはこんな感じです。

import Vue from 'vue'
import Router from 'vue-router'
import Vuex1 from './components/Vuex1Component.vue'
import Vuex2 from './components/Vuex2Component.vue'

Vue.use(Router)

export default new Router({
    mode: 'history',
    routes: [
        {
            path: '/vuex1',
            name: 'vuex1',
            component: Vuex1,
        },
        {
            path: '/vuex2',
            name: 'vuex2',
            component: Vuex2,
        },
    ],
})

以上を踏まえて

①VUEX1(Vuex1Component.vue)で名前を変更します。
②VUEX2(Vuex2Component.vue)でstoreから取得して表示し保持していることを確認します。
③VUEX1に戻ってきたときもcreatedでstoreから取得して保持していることを確認します。

<template>
<div class="mx-auto">

  <v-tabs v-model="active_tab">
    <v-tab>
      vuex1
    </v-tab>
    <v-tab :to="{ path: '/vuex2' }">
      vuex2
    </v-tab>
  </v-tabs>

  <v-card
    max-width="344"
  >
    <v-card-title><v-icon large>mdi-account-box</v-icon>{{ name }}</v-card-title>
    <v-card-text>
      <v-form v-model="valid">
        <v-text-field
          v-model="user.name"
          label="Name"
          :rules="[v => !!v || 'Name is required']"
          style="width: 300px;"
        ></v-text-field>
      </v-form>
    </v-card-text>
    <v-card-actions>
      <v-btn :disabled="!valid" @click="update" color="primary">UPDATE</v-btn>
    </v-card-actions>
  </v-card>

</div>
</template>

<script>
  export default {
    data() {
      return {
        valid: false,
        active_tab: 0,
        name: 'Nobody',
        user: {
          name: '',
        },
      }
    },
    created() {
      const name = this.$store.getters["user/user"].name
      if (name) {
        this.name = name;
      }
    },
    methods: {
      update() {
        this.$store.dispatch('user/setUser', this.user)
        this.name = this.user.name
      }
    },
  }
</script>
<template>
<div>

  <v-tabs v-model="active_tab">
    <v-tab :to="{ path: '/vuex1' }">
      vuex1
    </v-tab>
    <v-tab>
      vuex2
    </v-tab>
  </v-tabs>

  <v-card
    max-width="344"
  >
    <v-card-title><v-icon large>mdi-account-box</v-icon>{{ name }}</v-card-title>
  </v-card>

</div>
</template>

<script>
  export default {
    data() {
      return {
        active_tab: 1,
        name: 'Nobody',
      }
    },
    created() {
      const name = this.$store.getters["user/user"].name
      if (name) {
        this.name = name;
      }
    },
  }
</script>
js-vue-vuetify-vuex-1

Vuexでv-model使うとダメみたい

ここまでで終了のはずでしたが・・・
UPDATEをクリックしてないのに変更されてしまう事象が発生しました!

js-vue-vuetify-vuex-2

どうやらv-modelは勝手にstateを直接変更するみたいですね。
何か方法があるかもしれませんが、v-modelを使わなくても可能なので修正しましょう。

        <v-text-field
          id="name"		// 変更
          label="Name"
          :rules="[v => !!v || 'Name is required']"
          style="width: 300px;"
        ></v-text-field>

・・・

    methods: {
      update() {
        this.user.name = document.getElementById('name').value	// 追加
        this.$store.dispatch('user/setUser', this.user)
        this.name = this.user.name
      }
    },

v-model=”user.name”をid=”name”に変更してUPDATEをクリックしたときに入力されている文字列をuser.nameに設定するだけですね。

js-vue-vuetify-vuex-3

別コンポーネントで同期

これまではcreatedで読み込んで表示してましたが、UPDATEをクリックしたと同時に別コンポーネントの名前も変更するように同期させてみましょう。
ヘッダにApp Barを設定して名前を表示させて同期させてみます。

  <div id="app">
    <v-app>
      <v-content>
        <v-container>
          <header-component></header-component>
          <router-view />
        </v-container>
      </v-content>
    </v-app>
  </div>
<template>
  <div>
    <v-app-bar
      color="deep-purple accent-4"
      dense
      dark
    >
      <v-toolbar-title>Brand</v-toolbar-title>
      <v-spacer></v-spacer>
      Hello! {{ name }}
    </v-app-bar>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        name: 'Nobody',
      }
    },
    computed: {
      username() {
        let name = this.$store.getters["user/user"].name
        if (!name) {
          name = 'Nobody'
        }
        return name
      },
    },
    watch: {
      username(val, old) {
          this.name = val
      }
    },
  }
</script>

「computed」にvuexのstoreに保存されている名前を取得するメソッドを作成し「watch」でそのメソッドの戻り値を監視することで変更されても最新の名前を表示させることができます。

js-vue-vuetify-vuex-4

後記

あまり使いどころを思いつかなかったのですが、Vuexの基本について触れてみました。
とりあえず、ユーザ情報の変更時にVuexを介してサーバに設定することで再読み込み無しでヘッダに設定したユーザ名を自動更新できますね。
もちろん、正しく変更できたことが前提ですが。