Vuexのワークフローを改めて✅

「これ、完全にVuex理解しちゃったよね」と最高に調子に乗ってしまっています。とはいえ、エンジニアでもない自分の「完全に理解した」なんてたかが知れているので、ダニングクルーガーの最初の局面が来て喜んでるんだな〜可愛いな〜くらいの気持ちで見ていただけると助かります。

今までVuexに関する[記事](https://subaru-hello.hateblo.jp/entry/2021/10/24/215145)を[何度か](https://subaru-hello.hateblo.jp/entry/2021/10/11/214349)書いているのですが、全然Vuexを理解できていなかったです。私は、同じ内容を擦って擦って擦りまくって頭に入れていく学習方法を採用しています。ようやく今回Vuexが理解できたって感じですね。

それでは、Vuexのワークフローをまとめていきたいと思います。

基本的なVuexの書き方

storeをexportし、Vueインスタンスに読み込ませる

#store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import analyze from './modules/analyze';
Vue.use(Vuex);

export default new Vuex.Store({
  // eslint-disable-next-line no-undef
  strict: process.env.NODE_ENV !== 'production',
  modules: {
    analyze,
  },
});

analyze.jsの大枠

import axios from '../../plugins/axios';
const state = {
 
};

const getters = {
 
};

const mutations = {
  
};

const actions = {

};

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions,
};

Vuexの単方向ワークフロー

Vue component⇨actions⇨mutations⇨state⇨getters⇨Vue component

ざっくりとVuexで状態管理がなされている流れを記述すると以下の通りになる。

データを変更して!(actionsがmutationsにcommitする)

⇨わかった、データを変更するね!(mutationsがactionsから渡ってきたデータを使ってstateを変更する)

⇨データを持ってる!(stateにあるデータが変更される)

⇨変更されたデータをコンポーネントに渡すね!(gettersがstateにあるデータをコンポーネントに渡す)

⇨Vuexのデータが欲しい!(ComponentのcomputedでmapGettersを使ってgettersを呼び出す。)

setterとしてのmutation、getterとしてのgettersといったところか。

もう少し抽象度を高めると下記の通りになる。

Vue component⇨actions⇨mutations⇨state⇨getters⇨Vue component

実際に使ってみる

自作中のポートフォリオにある、Analyzeモデルを使用してまとめていこうと思う。

今回実現させたい動きは、

「診断ページでユーザーが行った診断内容を保存し、診断結果表示画面で、さっき保存された診断内容を表示させる。」

になる。

もっと抽象度を上げると、

actionsでaxiosのpostメソッドを使用し診断内容をmutationsに対して渡す。

mutationsは渡された診断内容をデータにsetして保存する。

遷移先画面では、axiosのgetメソッドを使用して診断内容をgetする。

各ファイルの設計状況

Analyzeテーブル

create_table "analyzes", charset: "utf8mb4", force: :cascade do |t|
    t.bigint "user_id"
    t.integer "total_points"
    t.integer "sake_strongness_types", default: 0, null: false
    t.integer "next_nomivation_types", default: 0, null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["user_id"], name: "index_analyzes_on_user_id"
  end

Analyzeモデルの状態を管理するanalyze .js

import axios from '../../plugins/axios';
const state = {
  analyzes: [
    {
      total_points: [],
      drunk_types: [],
      resistance_types: [],
    },
  ],
};

const getters = {
  analyzes: (state) => state.analyzes,
};

const mutations = {
  setAnalyze(state, analyzes) {
    state.analyzes = {
      total_points: analyzes.total_points,
      sake_strongness_types: analyzes.sake_strongness_types,
      next_nomivation_types: analyzes.next_nomivation_types,
     
    };
  },
  addAnalyze: (state, analyze) => {
    const analyzeArray = [];
    analyzeArray.push(analyze.total_points);
    analyzeArray.push(analyze.sake_strongness_types);
    analyzeArray.push(analyze.next_nomivation_types);
    state.analyzes = analyzeArray;
  },
};

const actions = {
  async fetchAnalyzes({ commit }) 
    const responseAnalyze = axios
      .get('analyzes')

      .then((responseAnalyze) => {
        commit('setAnalyze', responseAnalyze.data);
      })
      .catch((err) => console.log(err.responseAnalyze));
  },
  async createAnalyze({ commit }, analyze) {
    try {
      const analyzeResponse = await axios.post('analyzes', { analyze: analyze });
      commit('addAnalyze', analyzeResponse.data);
      return analyzeResponse.data;
    } catch (err) {
      console.log(err);
      return nil;
    }
  },
  async updateAnalyze({ commit }, updAnalyze) {
    const response = await axios.put(`analyzes/${updAnalyze.id}`, updAnalyze);
    commit('updateAnalyze', response.data);
  },
};

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions,
};

診断画面を担うAnalyze.vue

					<template>
					  <div>
					    <v-row justify="center" align-content="center">
					      <v-col
					        v-for="question in questions"
					        :key="question.num"
					        cols="12"
					        xs="12"
					        sm="12"
					        md="12"
					        lg="12"
					      >
					      ...
									<v-card-title>
                    Q{{ question.num }}.
                    {{ question.title }}
                  </v-card-title>
										...
                  <v-radio-group >
                    <v-radio
                    ...
                      @click="clickScroll($event); checkAnswer(question.num, 1); "
                      label="1: いつもある"
                    ></v-radio>
                    <v-radio
											...
                      @click=" clickScroll($event); checkAnswer(question.num, 2);"
                      label="2: 時々ある"
                    ></v-radio>
                    <v-radio 
											...
											@click=" clickScroll($event);checkAnswer(question.num, 3); "
                      label="3: 全くない"
                    ></v-radio>
                  </v-radio-group>
                  <p class="py-3" style="font-size: 16px">あなたの回答: {{ question.answer }}</p>
                </v-card>
              </v-container>
            </v-card-title>
          </v-layout>
        </v-col>
      </v-col>
		...
			     <v-col cols="4" xs="4" sm="2" md="2" lg="1">
			        <v-btn
			          style="font-size: 30px"
			          @click="clickScrollNext()"
			        >
			          次へ
		        </v-btn>
		      </v-col>
		    </v-row>
		   <v-col @click="setShuchedule()" >
                <p style="font-size: 32px; text-decoration: none; text-color: black">ほろ酔い</p>
                <img :src="sakeSrc" width="150" height="100" />
              </v-col>

              <transition name="modal">
                <div v-if="showModal" @close="showModal = false">
                  <div class="modal-mask">
                    ...
                    <p style="font-size: 32px">酒ケジュール作成中...</p>
                  </slot>
                </div>
              </div>
            </div>
    ...
</template>
<script>
import axios from '../plugins/axios';
import { mapGetters, mapMutations, mapActions } from 'vuex';
export default {
  data() {
    return {
      isVisible: '',
      dialog: false,
      show: false,
      showModal: false,
    };
  },
  components: {
    FacebookLoader,
  },
  computed: {
    ...mapGetters('question', ['questions']),
    ...mapGetters('analyze', ['analyzes']),

   ...

    createSchedule() {
      var currentAnalyzes = this.analyzes;
      var trueAnswers = this.questions;
      const answer0 = trueAnswers[0]['answer'];
      const answer1 = trueAnswers[1]['answer'];
      const answer2 = trueAnswers[2]['answer'];
      const answer3 = trueAnswers[3]['answer'];
      const answer4 = trueAnswers[4]['answer'];
      const answer5 = trueAnswers[5]['answer'];
					 ...
      const sumResult =
        answerFirst +
        answerSecond +
       ...
      let AlcoholStrongness = sumResult > 0 ? 2 : sumResult === 0 ? 1 : 0;
      currentAnalyzes.total_points = sumResult;
      currentAnalyzes.sake_strongness_types = AlcoholStrongness;

      return currentAnalyzes.sake_strongness_types;
    },
  },
  methods: {
    //同期処理を記述する
    ...mapMutations('question', ['updateAnswer']),
    ...mapMutations('analyze', ['addAnalyze']),
    ...mapActions('analyze', ['createAnalyze']),

    checkAnswer(indexNum, updAnswer) {
      this.updateAnswer({ indexNum, updAnswer });
    },
    async setShuchedule() {
      var trueAnswers = this.questions;
      const answer0 = trueAnswers[0]['answer'];
      ...
      let answerFirst = answer0 === 1 ? -10.04 : answer0 === 2 ? 8.95 : 5.22; //重要
      let answerSecond = answer1 === 1 ? -0.43 : answer1 === 2 ? -2.98 : 1.2;
     ...
      const sumResult =
        answerFirst +
        answerSecond +
        answerThird +
       ...
      let AlcoholStrongness = sumResult > 0 ? 2 : sumResult === 0 ? 1 : 0; //2: 酒豪, 1: 普通. 0: 下戸
      let Nomivation = 0;

      let promise = new Promise((resolve, reject) => {
        // #1
        const updAnalyze = {
          total_points: sumResult,
          sake_strongness_types: AlcoholStrongness,
          next_nomivation_types: Nomivation,
        };

        resolve(this.createAnalyze(updAnalyze));
        reject();
      });
      promise
        .then(() => {
          // #2
          return new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve((this.showModal = true));
              reject();
            }, 1000);
          });
        })
        .then(() => {
          // #3
          return new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve(this.$router.push('/result'));
              reject(console.log());
            }, 3200);
          });
        })
        .catch(() => {
          // エラーハンドリング
          console.error('Something wrong!');
        });
      return promise;
    },
   ...
  },
};
</script>

診断結果を担うResult.vue

<template>
<h1 class="text-center" style="font-size: 50px">
      あなたは
      <p class="text-center" style="font-size: 50px">
        {{ Sakenotuyosa }} です。<img :src="beerSrc" width="150" height="100" />
      </p>
      {{NextNomikaiMotivation}}に向けた酒ケジュール
    </h1>
</template>
<script>
export default {
data: function () {
    return {
      analyzes: [],
    };
  },
computed:{
Sakenotuyosa() {
      const currentAnalyze = this.analyzes;
      const yourAnalyze = currentAnalyze[currentAnalyze.length - 1]["sake_strongnes"];
      if (yourAnalyze === 'weak') {
        return '下戸';
      } else if (yourAnalyze === 'normal') {
        return '普通';
      } else {
        return '酒豪';
      }
    },
NextNomikaiMotivation(){
  const currentAnalyze = this.analyzes;
      const yourAnalyze = currentAnalyze[currentAnalyze.length - 1]["next_nomivation"];
      if (yourAnalyze === flesh') {
        return 'しっぽり';
      } else if (yourAnalyze === 'tipsy') {
        return 'ほろ酔い';
      } else {
        return '酩酊';
      }
}
},
  mounted() {
     axios.get('/analyzes').then((analyzeResponse) => (this.analyzes = analyzeResponse.data));
  },
created() {
    this.fetchAnalyzes();
  },
methods: {
    ...mapActions('analyze', ['fetchAnalyzes']),
    ...mapActions('analyze', ['createAnalyze']),
    ...mapActions('users', ['fetchAuthUser']),
  },

}
</script>

それぞれの役割をまとめる

state

Analyzeモデルのデータを保存しておく場所。空配列にしておく。

actionsがAPIを叩いてfetchしてきたanalyzesデータを格納するために空配列にしている。

直接stateを変更することは許されない行為。

今回はanalyzeモデルにある3つのデータを格納するための空配列が用意されている。

const state = {
  analyzes: [
    {
      total_points: [],
      drunk_types: [],
      resistance_types: [],
    },
  ],
};

this.$store.state.analyzesと書いてコンポーネントに呼び出すこともできるが、直接stateを持ってくることはあまり推奨されていないみたい。

getters

コンポーネントからstateを参照したい時に経由する場所。その名の通り、getter。

リアクティブに変更されたデータにアクセスすることができる。

stateを引数に取り、stateにあるanalyzesデータにアクセスしている。

#store/analyze.js
 const getters = {
  analyzes: (state) => state.analyzes,
};

下記のように書くだけで、DBからデータを持ってくることができる。

#Analyze.vue

<script>
export default {
data: {},
computed{
 ...mapGetters('analyze', ['analyzes']),
}
}
</script>

取ってきた値は、createdで呼び出しておく。このcreatedの段階ではDOMが構築されていない。

DOM構築後にサーバーにアクセスするよりもサーバーに対する負担が少なくなるので、APIからデータを持ってくるときはmountedよりcreatedに記述することが推奨されている

#Result.vue
<script>
export default {
data: function () {
    return {
      analyzes: [],
},
computed{
 ...mapGetters('analyze', ['analyzes']),
},
created() {
    this.fetchAnalyzes();
  },
methods: {
    ...mapActions('analyze', ['fetchAnalyzes']),
}
}
</script>

actionsで定義されているaddAnalyzeのおかげで、DBから持ってきたデータをコンポーネント内のdataに格納することができる。このデータはthis.analyzesで参照が可能。


const actions ={ 
  addAnalyze: (state, analyze) => {
    const analyzeArray = [];
    analyzeArray.push(analyze.total_points);
    analyzeArray.push(analyze.sake_strongness_types);
    analyzeArray.push(analyze.next_nomivation_types);
    state.analyzes = analyzeArray;
  },
}

mutations

stateの状態を変更できる唯一の場所。

「何らかのイベント(クリックやインプット等)が発火した時にデータを変更する」ような同期処理を記述する。

直接mutationを動かそうとするとエラーが吐き出されるので注意

#store/analyze.js
const mutations = {
  setAnalyze(state, analyzes) {
    state.analyzes = {
      total_points: analyzes.total_points,
      sake_strongness_types: analyzes.sake_strongness_types,
      next_nomivation_types: analyzes.next_nomivation_types,
     
    };
  },
  addAnalyze: (state, analyze) => {
    const analyzeArray = [];
    analyzeArray.push(analyze.total_points);
    analyzeArray.push(analyze.sake_strongness_types);
    analyzeArray.push(analyze.next_nomivation_types);
    state.analyzes = analyzeArray;
  },
};

methods内で呼び出す。

そうすることで、methods内でthis.setAnalyzeやthis.addAnalyzeとして使うことができる。

methods: {
    ...mapMutations('analyze', ['addAnalyze']),

actions

mutationを起動させる。外部APIを叩くような非同期通信をする。

ここでaxiosを使った非同期通信を記述していく。

今回analyze.jsで定義したactionsでは、async/awaitを使ってjavasccriptの処理順番を制御している。

fetch、create、updateとそれぞれAnalyzeの前につけて命名をしている。

それぞれの役割は下記の通りになる。

  • fetchAnalyze: データをDBからgetしてくる処理(controllerのindexアクションが使われる)
  • createAnalyze: ユーザーの入力した値をDBにpostする処理(controllerのcreateアクションが使えわれる)
  • updateAnalyze: データを上書きして保存する処理(controllerのupdateアクションが使われる)
const actions = {
  async fetchAnalyzes({ commit }) 
    const responseAnalyze = axios
      .get('analyzes')

      .then((responseAnalyze) => {
        commit('setAnalyze', responseAnalyze.data);
      })
      .catch((err) => console.log(err.responseAnalyze));
  },
  async createAnalyze({ commit }, analyze) {
    try {
      const analyzeResponse = await axios.post('analyzes', { analyze: analyze });
      commit('addAnalyze', analyzeResponse.data);
      return analyzeResponse.data;
    } catch (err) {
      console.log(err);
      return nil;
    }
  },
  async updateAnalyze({ commit }, updAnalyze) {
    const response = await axios.put(`analyzes/${updAnalyze.id}`, updAnalyze);
    commit('updateAnalyze', response.data);
  },
};

mutations同様、methodsで呼び出す。

this.createAnalyzeのように使うことができる。

#Analyze.vue
methods:{

  ...mapActions('analyze', ['createAnalyze']),
 let promise = new Promise((resolve, reject) => {
         #1
        const updAnalyze = {
          total_points: sumResult,
          sake_strongness_types: AlcoholStrongness,
          next_nomivation_types: Nomivation,
        };
			#成功したらcreateAnalyzeにupdAnalyzeを入れてstateを変更する。
        resolve(this.createAnalyze(updAnalyze));
        reject();
      });
      promise
        .then(() => {
           #2
					 #1が成功したら、resultページに遷移させる処理
          return new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve(this.$router.push('/result'));
              reject(console.log());
            }, 3200);
          });
        })
        .catch(() => {
          // エラーハンドリング
          console.error('Something wrong!');
        });
      return promise;
    },

}

まとめ

  • Vue component⇨actions⇨mutations⇨state⇨getters⇨Vue componentの流れでワークが進んでいく。
  • データを変更するメソッド(actions,mutations)はmethodsで呼び出し、状態を管理するメソッド(state,getters)はcomputedで呼び出す。
  • createdでデータをfetchしてくる。

あとがき

なんか、もっとこう、うまく説明できるようになりたいな。。

ステート | Vuex

ゲッター | Vuex

アクション | Vuex

ミューテーション | Vuex

Vuexをわかりやすく知りたい|あきな@旅、本、プログラミング。|note

改めて学び直すVuex - Qiita