MORITOMOMENT

登山好きエンジニアのテックブログ

プログラミング・アウトドア関連を中心に発信

Vuexの状態管理でハマった話 - アクションを実行しても状態が反映されない

f:id:moritomo7315:20200426155457p:plain

こんにちは、最近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
  }
})

簡単にソースコードの内容を言葉で説明しておくと

  1. 初期状態ではユーザのloginStatusという状態はfalse
  2. Headerにはログインと会員登録のリンクがある
  3. ログイン画面ではEmail・passwordの入力があるが意味はない
    (認証は省略しているため、必須化もしてない)
  4. ログインボタンを押すと、loginアクションが実行される
  5. 上記アクションにより、ユーザのloginStatusがfalseからtrueに変わる
  6. トップページに遷移し、Headerにはマイページとログアウトのリンクがある
  7. ログアウトするとユーザの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の概念を理解しきっていないので、

説明に使用してる言葉があやしいところもあるかもしれませんが、

無事解決できました。