Vuex
What is Vuex
- State を予見可能な形で集中管理する
- 中規模・大規模なアプリに最適
- 小規模であればstore パターンで事足りるかも
はじめに
Vuex を使うのと、グローバルオブジェクトを使う場合との違い
- Vuex の store から取得した値はリアクティブになる
- Vuex の値は mutation を commit することによってのみ変更できる。これにより値の変更が追跡可能になる。
基本的な使い方
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++;
},
},
});
// mutationをコミットする
store.commit('increment');
// storeの値を取得する
console.log(store.state.count); // => 1
State
Single State Tree
Vuex の State は単一のオブジェクトで管理される。
コンポーネントで store を使う
// let's create a Counter component
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count() {
return store.state.count;
},
},
};
computed プロパティとすることで、store の更新を検知し、コンポーネントをアップデートできるようになる。
上記のやり方はスケールしないので、通常は下記のようにする。
// root Vue インスタンス
const app = new Vue({
// provide the store using the "store" option.
// this will inject the store instance to all child components.
store,
/* ... */
});
// コンポーネント
const Counter = {
computed: {
count() {
return this.$store.state.count;
},
},
};
mapState
react-redux の mapStateToProps と同じ機能。
this.$store.state
配下の各値をコンポーネントのcomputed
に手早くマップするときに使う。
import { mapState } from 'vuex';
export default {
computed: {
...mapState({
// ファンクションを使う方法
count: (state) => state.count,
// storeのキー名を使う方法
countAlias: 'count',
// ローカルStateと組み合わせる場合はノーマル関数を使う
countPlusLocalState(state) {
return state.count + this.localCount;
},
}),
},
// もしくは、キー名を配列で渡す方法もある
computed: {
...mapState([
// map this.count to store.state.count
'count',
]),
},
};
ローカルの computed と一緒に使う場合は下記のようにする。
computed: {
...mapState({})
localComputed () { /* ... */ },
}
Getters
- state の getter を設定することができる。computed の store 版と思えば OK。
- getter の再計算は、依存する値が変更された時のみ行われる(computed と一緒の挙動)
const store = new Vuex.Store({
state: {
count: 1,
},
getters: {
bigCount: (state) => state.count * 10,
// 第2引数に他のgetterを取ることもできる
moreBigCount: (state, getters) => getters.bigCount * 10,
},
});
Getter へのアクセス
コンポーネントからはthis.$store.getters
で取得できる。
computed: {
bigCount () {
return this.$store.getters.bigCount
}
}
応用編(メソッドスタイル)
getter がファンクションを返すようにすることで、コンポーネント側からクエリを投げることができる。
// store側
getters: {
getTodoById: (state) => (id) => {
return state.todos.find((todo) => todo.id === id);
};
}
// コンポーネント側
store.getters.getTodoById(2); // -> { id: 2, text: '...', done: false }
mapGetters
state と同じく、this.$store.getters
配下の各値を手早くコンポーネントのcomputed
にマップする時に使う。
import { mapGetters } from 'vuex'
export default {
computed: {
// 名前を変更せずにそのままマップする 時
...mapGetters([
'doneTodosCount',
'anotherGetter',
])
// 名前を変更してマップしたいとき
...mapGetters({
doneCount: 'doneTodosCount'
})
}
}
Mutations
各 mutation はtypeとhandlerを持つ。handler は第一引数にstate
をとる。
const store = new Vuex.Store({
mutations: {
// ファンクション名をstringにしたものがtypeである => 'increment'
// ファンクション自身が、handlerである
increment(state) {
// mutate state
state.count++;
},
},
});
mutation は直接実行することはできない。実行するときは下記のようにする。これがmutation
をcommit
するということ。
// 'increment' typeのmutationを実行してくださいよー
store.commit('increment');
payload
mutation を commit する時に、引数を渡すことができる。これがpayload
である。
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
Object-Style Commit
store.commit()
には、string ではなくオブジェクトを渡すこともできる。
この場合、このオブジェクトが全て payload として handler に渡される。
store.commit({
type: 'increment',
amount: 10,
});
ハンドラを作成するときの注意点
Vuex の state に関する注意点は、Vue の data に関する注意点と同じ。
- 必要な State は、はじめから全てセットしておく(少なくとも初期値は)。 後から追加しないこと。
- やむなく新しいプロパティをオブジェクトに追加するときは、下記のいずれかの手段を取る。
Vue.set(obj, 'newProp', 123)
を使うstate.obj = { ...state.obj, newProp: 123 }
な どし、新しいオブジェクトに置き換える
type に定数を使う
redux の慣習と同じように、vuex でも mutation types に定数を指定する慣習がある。
// store.js
import Vuex from 'vuex';
import { SOME_MUTATION } from './mutation-types';
const store = new Vuex.Store({
mutations: {
// we can use the ES2015 computed property name feature
// to use a constant as the function name
[SOME_MUTATION](state) {},
},
});
mutations で非同期処理はできない
- なぜか?devtool がキャプチャする時にややこしいから。
- mutation は、redux の reducers と同じ立ち位置にいる。
mapMutations
this.$store.commit('***')
でアクセスできるものの、通常は、mapMutations
を使ってコンポーネントのmethods
にマップしてつかう。
import { mapMutations } from 'vuex';
export default {
methods: {
...mapMutations([
// map `this.increment()` to `this.$store.commit('increment')`
'increment',
// `mapMutations` also supports payloads:
// map `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
'incrementBy',
]),
...mapMutations({
add: 'increment', // map `this.add()` to `this.$store.commit('increment')`
}),
},
};
Action
構造は Mutation とほぼ同じであるものの、下記の点が異なる。
- Action は非同期処理を行える
- Action は Store にアクセスできる。Mutation は state にしかアクセスできない。
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++;
},
},
actions: {
increment(context) {
context.commit('increment');
},
},
});
context
はstore
インスタンスとほぼ同じなので、たとえば下記が使える。
context.commit
context.state
context.getters
context.dispatch
など
下記のように destructuring を使うことが多い。
actions: {
increment ({ commit }) {
commit('increment')
}
}
action の dispatch
基本的に mutation と同じ。commit
がdispatch
になっただけ。
// simple dispatch
store.dispatch('increment');
// dispatch with a payload
store.dispatch('incrementAsync', {
amount: 10,
});
// Object-Style
store.dispatch({
type: 'incrementAsync',
amount: 10,
});
payload を渡した場合は下記のように受け取れる。これも mutation とおなじ。
actions: {
increment (context, payload) {},
}
mapActions
これも mutation と同じ。this.$store.dispatch('***')
をコンポーネントの methods に手早くマップするために使う。
import { mapActions } from 'vuex';
export default {
methods: {
...mapActions([
// map `this.increment()` to `this.$store.dispatch('increment')`
'increment',
// `mapActions` also supports payloads:
// map `this.incrementBy(amount)` to `this.$store.dispatch('incrementBy', amount)`
'incrementBy',
]),
...mapActions({
add: 'increment', // map `this.add()` to `this.$store.dispatch('increment')`
}),
},
};
複数の action を組み合わせて使う
// assuming `getData()` and `getOtherData()` return Promises
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // wait for `actionA` to finish
commit('gotOtherData', await getOtherData())
}
}
Modules
Vuex の Store は、モジュール単位に分けることができる。
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state
Module Local State
モジュールの中で、Local State(モジュール自身の State)と Root State(親の State)にアクセスする方法は次の通り。
const moduleA = {
getters: {
someGetter(state, getters, rootState, rootGetters) {},
},
actions: {
someAction({ state, getters, rootState, rootGetters }) {},
},
mutations: {
someMutation(state) {}, // mutationsはローカルStateにしかアクセスできない!
},
};
getters
やactions
は Root State にアクセスできるものの、mutations
はローカル State にしかアクセスできない。モジュールの State の変更は、あくまでそのモジュールの Mutation でのみで行う。
Name Spacing
- デフォルトでは、モジュール内の
getters
,mutations
,actions
はグローバルな Namespace にそのまま登録される(他のモジュールから容易にアクセスできる)。一方、namespaced
キーを true にすると、モジュール名/ファンクション名
の形で登録される。 - Namespace が有効なモジュール内の getter と actions は、ローカライズされた
getters
,dispatch
,commit
を受け取る。これは、モジュール内の他のアセットに Prefix なしでアクセスできることを意味する。
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// module assets
state: { ... }, // module state is already nested and not affected by namespace option
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// nested modules
modules: {
// inherits the namespace from parent module
myPage: {
state: { ... },
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// further nest the namespace
posts: {
namespaced: true,
state: { ... },
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
Namespaced Modules 内からグローバル Assets にアクセスする
- 親の state と getters にアクセスしたいときは、
rootState
,rootGetters
を使う。 - 親の
dispatch
とcommit
を使いたいときは、{root: true}
オプションを指定する。
modules: {
foo: {
namespaced: true,
getters: {
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},
actions: {
someAction ({ state, getters, rootState, rootGetters, dispatch, commit }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}
Namespaced モジュール内でグローバルな Action を登録する。
Namespaced 内で Prefix なしのアクションを登録する方法。混乱を招くだけの機能のような気がする。
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}