Vuexの状態管理でハマった話 - アクションを実行しても状態が反映されない
こんにちは、最近Nuxt.jsで個人開発をしているモリトモです。
はじめてフロントエンドのフレームワークを触ることもあって、
なかなか状態管理という概念に苦しまされております。
今回は、
loginフォームからログインをしてトップページへ遷移するときに、ユーザのステータス状態がログアウトからログインに切り変わらない
という問題に直面して、
何が原因でうまく状態が反映されなかったのかを備忘録としてまとめたいと思います。
前提条件
環境
- Mac OS
- Nuxt.js v2.12.2
- vuex v3.1.3
簡略化したディレクトリ構成
ルート ├── components │ ├── emailSignin.vue │ └── header.vue ├── pages │ ├── README.md │ ├── index.vue │ └── login.vue ── store ├── index.js └── modules └── user.js
解決前のソースコード
components/header.vue(クリックで折りたたみを展開)
<template> <div> <v-app-bar color="primary" > <v-toolbar-title> <nuxt-link to='/'> <img src="~/static/weblogo.png"> </nuxt-link> </v-toolbar-title> <v-spacer></v-spacer> <v-menu offset-y> <template v-slot:activator="{ on }"> <v-btn icon color="transparent" v-on="on" > <v-app-bar-nav-icon></v-app-bar-nav-icon> </v-btn> </template> <v-list v-if="loginStatus"> <v-list-item nuxt to='#' > <v-list-item-title>マイページ</v-list-item-title> </v-list-item> <v-list-item @click="logout" nuxt to='/' inactive > <v-list-item-title>ログアウト</v-list-item-title> </v-list-item> </v-list> <v-list v-else> <v-list-item nuxt to='/login' > <v-list-item-title>ログイン</v-list-item-title> </v-list-item> <v-list-item nuxt to='/signup' > <v-list-item-title>会員登録</v-list-item-title> </v-list-item> </v-list> </v-menu> </v-app-bar> </div> </template> <script> import { mapState, mapGetters, mapActions } from 'vuex'; export default { name: "Header", computed: { ...mapState({ user: state => state.user.user, loginStatus: state => state.user.loginStatus }) }, methods: { ...mapActions('user',[ "logout" ]) } } </script>
components/emailSignin.vue(クリックで折りたたみを展開)
<template> <div class="userpage"> <h1>ログイン</h1> <div class="userform"> <v-form> <v-text-field v-model="email" label="E-mail" outlined required ></v-text-field> <v-text-field v-model="password" label="password" outlined type="password" required ></v-text-field> <v-btn color="secondary" class="mr-4" @click="login" x-large nuxt to="/" > ログイン </v-btn> <nuxt-link to="#" > パスワードを忘れた方はこちら... </nuxt-link> </v-form> </div> </div> </template> <script> // import firebase from "@/plugins/firebase" firebaseは今回は省略してる import { mapState, mapGetters, mapActions } from "vuex"; export default { name: "EmailSignin", data() { return { email: '', password: '', } }, computed: { ...mapState({ user: state => state.user.user, loginStatus: state => state.user.loginStatus }) }, methods: { ...mapActions('user',[ "login" ]), passworLogin () { this.login() // f状態管理の動作確認のためirebaseは無しにしてる // firebase.auth().signInWithEmailAndPassword(this.email, this.password) // .then( () => { // this.login() // }) // .catch((error) => { // alert(error.message) // }) } } } </script>
pages/login.vue (クリックで折りたたみを展開)
<!-- EmailSigninコンポーネントを呼び出してるだけ。今後Twitterログイン等も入れたいので、コンポーネントに分けている --> <template> <EmailSignin /> </template> <script> import EmailSignin from '~/components/emailSignin' export default { components: { EmailSignin } } </script>
store/modules/user.js (クリックで折りたたみを展開)
const state = { user: null, loginStatus: false } const getters = { user: (state) => state.user, isLogin: (state) => state.loginStatus } const mutations = { setUser(state, { user }) { state.user = user }, login(state) { state.loginStatus = true }, logout(state) { state.loginStatus = false state.user = null } } const actions = { fetchUser({ commit }, user) { commit('setUser', {user}) }, login({ commit }) { commit('login') }, logout({ commit }) { commit('logout') } } export default { namespaced: true, state, getters, actions, mutations }
store/index.js (クリックで折りたたみを展開)
import Vuex from 'vuex' import user from './modules/user' export default () => new Vuex.Store({ modules: { user } })
簡単にソースコードの内容を言葉で説明しておくと
- 初期状態ではユーザのloginStatusという状態はfalse
- Headerにはログインと会員登録のリンクがある
- ログイン画面ではEmail・passwordの入力があるが意味はない
(認証は省略しているため、必須化もしてない) - ログインボタンを押すと、loginアクションが実行される
- 上記アクションにより、ユーザのloginStatusがfalseからtrueに変わる
- トップページに遷移し、Headerにはマイページとログアウトのリンクがある
- ログアウトするとユーザのloginStatusがtrueからfalseに変わる
起きてしまってる現象
上記の4のアクションは実行されるが5の状態の変更が反映されない
解決策
上手くいかない原因と解決策
問題点は非常に単純ではありますが、見つけるのに苦労しました。
@click
@clickでログインボタンを押した時にアクションが実行されますが、
このときコンポーネントの呼び出しの親子関係は
(親) login.vue -> emailSignin.vue (子)
というようになっています。
ユーザ的には、login.vueを見ているわけで、実際にボタンはlogin.vueで使用されてるemailSigninコンポーネントの中にあります。
ログインボタン(emailSignin)によって発火するイベントをlogin.vueでも発生させるためには
@click="login" ↓ @click.native="login"
に変更しないといけないみたいです。
要するに、
子コンポーネントのイベントを
親コンポーネントで発生させるためには
上記の記述が必要ということですね。
なので、/components/emailSignin.vueを
<v-btn color="secondary" class="mr-4" @click="login" x-large nuxt to="/" > ログイン </v-btn> ↓↓↓ <v-btn color="secondary" class="mr-4" @click.native="login" x-large nuxt to="/" > ログイン </v-btn>
補足:ログアウトについて
ログアウトについてはHeaderコンポーネントで起きるアクションであり、
userの状態を管理してるルートのコンポーネントになっているため、
@clickのみでもイベントは起きます。
まとめ
@clickでアクションイベントを発火させる場合は、
コンポーネントの親子関係を意識して、
そのコンポーネントに応じて
@clickか@click.native
を指定する必要があります。
まだvuexの概念を理解しきっていないので、
説明に使用してる言葉があやしいところもあるかもしれませんが、
無事解決できました。