LoginSignup
10
8

More than 3 years have passed since last update.

頑張らないオレオレVuex規約を作った話

Posted at

はじめに

この記事は、Vue.js Advent Calendar 2019 #20日目 の記事です。
担当は @is_ryo でお送りします。

Vuexの治安悪すぎ問題

みなさん Vuex の設計ってどうされていますか?
規約を設けずに自由な感じで Vuex を使っているとこんなことありませんか?

  • 巨大化する State…
  • 直接呼ばれるmutation…
  • getters?actions?なにそれおいしいの?
  • modulesが分けられてない…
  • とりあえず Vuex に突っ込んでおくかーみたいな風潮

といった悩みを抱えてませんか?私は日々悩まされています…
Vuex はかなり便利でいろんな使い方ができるので治安が悪くなりがちなのかなと思います。
実際に私のチームでも Vuex 周りの治安がかなり悪くなってしまいました…

規約を作ろう

やっぱり治安を良くするためにはまず規約を作らないといけませんよね。
でもあんまりがちがちな規約を作ってしまうと守ってもらえなくなってしまう…
なので下記3点を解決できるような、かつあんまり頑張らない規約を作りました。

  • Storeの中身を整理したい
  • 整理した上でComponentがどのStoreを使っているのかわかりやすくしたい
  • Storeに Store.hoge でアクセスしたくない

どんなの作った?

ざっと下記の5つ

  • stores/ 以下にVuex関係のソースを置く
  • namespaceを切って、それぞれのnamespace(Module)ファイルを生成する
  • Moduleの構成要素は、namespaced / state / mutations / actions (/ getters)
  • 直接 mutations を使わない
  • createNamespacedHelpersを使って、Storeとやり取りする

stores/ 以下にVuex関係のソースを置く

ディレクトリ構成はこんな感じ

src/stores/
├── flags.ts
├── store.ts
└── types.d.ts

Storeの親ファイル。modules 内にModuleファイルをimportしていく。
modules内にModuleを入れていく。

import Vue from "vue"
import Vuex, { StoreOptions } from "vuex"
import { RootState } from "./types"
import { flags } from "./flags"

Vue.use(Vuex)

const store: StoreOptions<RootState> = {
  state: {
    version: ""
  },
  modules: {
    flags  // ←ここにModuleを突っ込む。この場合のnamespace名はflags
  }
}

export default new Vuex.Store<RootState>(store) // ←Storeを生成

flags.ts

StoreのModuleファイル。ここにModuleの内容を書いていく。
Moduleを変数に入れてexportする。そのときの変数名がnamespace名となる。

import { Module, ActionContext } from "vuex"
import { FlagsState, RootState } from "./types"

export const flagsNamespacedHelper = createNamespacedHelpers("flags")

export const flags: Module<FlagsState, RootState> = {
  /* 省略 */
}

types.d.ts

Store関係の型ファイル。

namespaceを切って、それぞれのnamespace(Module)ファイルを生成する

すべてひとつのStoreとして扱うとStateがゴミ箱みたいになってくるので、namespaceを切ります。
namespaceを切ることでModuleとしてStoreを分割することができます。
Storeで扱いたいデータの種別ごとで切るといい感じになると思います。
例えばフラグ関係とか描画するデータ関係とか…

Moduleの構成要素は、namespaced / state / mutations / actions (/ getters)

Moduleの構成要素は、namespaced / state / mutations / actions (/ getters)になります。(gettersは必要ないかも)
例: stores/flags.ts

import { Module, ActionContext, createNamespacedHelpers } from "vuex"
import { FlagsState, RootState } from "./types"

export const flagsNamespacedHelper = createNamespacedHelpers("flags")

export const flags: Module<FlagsState, RootState> = {
  namespaced: true,
  state: {
    hiddenToolbarItems: false,
    overlay: false
  },
  mutations: {
    setHiddenToolbarItems(state: FlagsState, value: boolean): void {
      state.hiddenToolbarItems = value
    },
    setOverlay(state: FlagsState, value: boolean): void {
      state.overlay = value
    }
  },
  getters: {
    hiddenToolbarItems(state: FlagsState): boolean {
      return state.hiddenToolbarItems
    },
    overlay(state: FlagsState): boolean {
      return state.overlay
    }
  },
  actions: {
    setHiddenToolbarItems(
      { commit }: ActionContext<FlagsState, RootState>,
      value: boolean
    ) {
      commit("setHiddenToolbarItems", value)
    },
    setOverlay(
      { commit }: ActionContext<FlagsState, RootState>,
      value: boolean
    ) {
      commit("setOverlay", value)
    }
  }
}

直接 mutations を使わない

直接 mutations を使わないようにします。要するに Store.commit("hoge") って書かないようにします。
あくまで個人の感覚なんですが mutations はprivateな関数として扱った方がいいのかなって思っています。
逆に actions はpublicな関数として扱うという形で mutationsactions の棲み分けをします。
actions 経由で mutations を使いましょう。書き方は下記のような感じです。

Store.dispatch("hogehoge")

NamespacedHelpersを使って、Storeとやり取りする

というかそもそも Store.hoge って書くのをやめましょう。
毎回 import Store from "@/stores/store" って書くのめんどくさすぎるし、どのModuleを使ってるのかわからないくて困るし、namespaceを切っているので、namespaceを間違ったり書き忘れたりするので…
そういうときに役立つのが NamespacedHelpers です。
Moduleを定義しているファイルの中で createNamespacedHelpers() を定義して export します。

import { createNamespacedHelpers } from "vuex"

export const flagsNamespacedHelper = createNamespacedHelpers("flags")

こうやってModuleに対してやり取りをできるようにできます。
Stateを購読する場合は、computedでmapStateを展開します。スプレッド構文で書く必要があるので注意。展開したあとは this から参照できます。
例: App.vue (抜粋)

import Vue from "vue"
import { createNamespacedHelpers } from "vuex"
import { flagsNamespacedHelper } from "@/stores/flags"

export default Vue.extend({
  computed: {
    ...flagsNamespacedHelper.mapState({
      hiddenToolbarItems: (state: any) => state.hiddenToolbarItems,
      overlay: (state: any) => state.overlay
    })
  }
})

Actionを使う場合は、methodsにmapActionsを展開します。こちらもスプレッド構文で書く必要があります。また、一度変数に入れないと型推論をしてくれないので注意。展開したあとは this から参照できます。
例: viwes/home/script.ts (抜粋)

import Vue from "vue"
import { HomeComponentState } from "@/types"
import { flagsNamespacedHelper } from "@/stores/flags"

// Vuex store helpers
const flagsMapActions = flagsNamespacedHelper.mapActions([
  "setHiddenToolbarItems",
  "setOverlay"
])

export default Vue.extend({
  data(): HomeComponentState {
    return {
      text: "hoge"
    }
  },
  created() {
    this.setHiddenToolbarItems(true)
    this.setOverlay(false)
  },
  methods: {
    ...flagsMapActions,
    getText(): string {
      return this.text
    }
  }
})

まとめ

今回作った規約をまとめると、

  • ちゃんとModule単位に分割することでStoreを整理する
  • NamespacedHelperを使うことでComponentがどのModuleを使っているのかimportでわかりやすくする
  • mapStatemapActions を使ってStoreにアクセスする

といったところを重きに置いてます。
規約はそれぞれのチームでどのような規約が必要になってくるのか変わってくると思います。
これから規約を作らないと…という感じで困っている人の参考になれば嬉しいです!
ではまた!!!

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8