vuex에서 state를 변화시키는 유일한 방법은 mutation을 통해서다.
vuex mutation은 이벤트와 비슷한데, Mutation은 type
, handler
를 갖는다.
handler 함수가 state에 변화를 일으키고, 이 핸들러 함수를 호출시키는 것 store.commit(type) 이다.
핸들러 함수는 첫 번째 인자로 state를 받는다.
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// mutate state
state.count++
}
}
})
그리고 이 핸들러 함수를 호출하는 건
store.commit('increment')
commit에 payload를 같이 넘기기
store.commit을 할 때, payload라고 불리는 인자를 전달할 수 있다.
주로 payload는 객체로 전달하여 그 의미를 잘 알 수 있도록 한다.
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
객체 스타일의 commit
type 프로퍼티를 추가하여 객체를 인자로 넣어 handler를 호출하는 방식도 있다. 이 때, handler 쪽은 변경하지 않아도 된다.
store.commit({
type: 'increment',
amount: 10
})
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
뷰의 반응형 규칙을 따르는 Mutation
vuex store의 state는 반응적이도록 설계되었다. 따라서, state가 변경이 되면 해당 state를 Observing하는 컴포넌트는 자동으로 업데이트된다.
따라서, mutaion을 쓸 때는 다음을 지켜줘야 한다.
Vue.set(obj, 'newProp', 123)
처럼 하는 방법과state.obj = { ...state.obj, newProp: 123 }
mutation type에 상수를 사용하기
다른 파일에 mutation의 type을 상수로 적어 export하고, store에서 import하는 방식으로 하기도 한다. 이는 linter 기능 사용, 자동완성 등 여러 이점을 준다.
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// we can use the ES2015 computed property name feature
// to use a constant as the function name
[SOME_MUTATION] (state) {
// mutate state
}
}
})
mutaion은 반드시 동기적(Synchronous)
중요한 포인트는 📌 mutation 핸들러 함수는 반드시 동기적이어야 한다는 것이다.
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
위 예제의 문제점은 devtool이 commit이 된 시점에 callback이 실행 되었는지 아닌지를 알 방법이 없다는 것이다. 이렇게 되면, callback에서 state를 변화시켜도 추적할 수 없다는 문제가 있다.
mapMutations
컴포넌트에서 mutation 시키기
mapMutations
store의 Mutation을 method로 자동으로 변화시켜준다. type을 그대로 사용하려면 mapMutations
에 스트링 배열을 넘기고, 이름을 바꿔 사용하려면 객체를 넘기면 된다.
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // map `this.increment()` to `this.$store.commit('increment')`
// `mapMutations` also supports payloads:
'incrementBy' // map `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // map `this.add()` to `this.$store.commit('increment')`
})
}
}
나의 예제
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const ADD_COUNT = 'ADD_COUNT';
const store = new Vuex.Store({
state: {
count: 100
},
mutations: {
[ADD_COUNT](state, payload) {
state.count += payload.num;
}
}
});
export default store;
<template>
<header id="header">
<button @click="addToCount({ num: 10 })">click here</button>
store : {{ storeCount }}
</header>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex';
export default {
computed: {
...mapState({
storeCount: 'count', // string으로 넘기면, state => state.count와 같다.
}),
},
methods: {
...mapMutations({ addToCount: 'ADD_COUNT' })
}
};
</script>
mutation은 반드시 동기적인 transaction이어야 하는데, 그럼 비동기 operation은 어떻게 할까? 바로 Action!
Action은 다음과 같은 특징을 갖는다.
액션 핸들러는 context
객체라는 것을 받는다.
이 context 객체는 store instance에서 쓰이는 메소드와 프로퍼티의 set과 동일한 것이다. 따라서, store.commit
을 하여 mutation을 시켰던 것처럼, 액션 안에서 context.commit
을 하여 mutation을 커밋할 수 있다.
스토어에 있는 State나 getter에 접근하려면 context.state
, context.getters
로 접근할 수 있다. 다른 액션을 context.dispatch
로 호출할 수도 있다.
정리하자면, 액션은 context
객체를 인자로 받으며,
액션안에서…
state 접근 : context.state
getters 접근 : context.getters
mutation commit : context.commit
다른 액션 호출 : context.dispatch
비구조화로 commit을 바로 가져와서 쓰기
context.commit을 반복해서 쓰지 않아도 된다.
actions: {
increment ({ commit }) {
commit('increment')
}
}
Dispatching Actions
액션은 store.dispatch
메소드로 호출된다.
store.dispatch('increment');
Payload를 같이 넘길 수 있는데, 이 때 payload는 객체이다.
// dispatch with a payload
store.dispatch('incrementAsync', {
amount: 10
})
// dispatch with an object
store.dispatch({
type: 'incrementAsync',
amount: 10
})
비동기API를 사용하고, 여러 mutation을 커밋하는 예제를 살펴보면,
actions: {
checkout ({ commit, state }, products) {
// save the items currently in the cart
const savedCartItems = [...state.cart.added]
// send out checkout request, and optimistically
// clear the cart
commit(types.CHECKOUT_REQUEST)
// the shop API accepts a success callback and a failure callback
shop.buyProducts(
products,
// handle success
() => commit(types.CHECKOUT_SUCCESS),
// handle failure
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
컴포넌트에서 Action dispatch하기 : mapActions
컴포넌트 내에서 액션을 dispatch하기 위해서는 다음과 같이 호출해야 한다.
this.$store.dispatch('xxx')
근데 또 이게 귀찮으니까, mapActions
라는 헬퍼를 사용한다. 이 헬퍼는 컴포넌트 메소드와 store.dispatch
를 매핑시켜준다.
아래 예제에서 this.increment()를 사용하면, increment 액션을 호출해주게 된다. 마찬가지로 payload를 넣어서 호출하게 할 수도 있다. 또한, 배열이 아닌 객체를 넣으면, 액션의 이름을 바꿀 수 있다.
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // map `this.increment()` to `this.$store.dispatch('increment')`
// `mapActions` also supports payloads:
'incrementBy' // map `this.incrementBy(amount)` to `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // map `this.add()` to `this.$store.dispatch('increment')`
})
}
}
Composing Actions
액션은 주로 비동기적이다. 그럼 어떻게 액션이 끝났는지를 알 수 있을까? 또한, 어떻게 여러 액션을 같이 구성할 수 있을까?
action을 dispatch하는 store.action
은 액션 핸들러가 반환하는 Promise를 핸들링할 수 있다.
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
store.dispatch('actionA').then(() => {
// ...
})
async/await
을 사용할 수도 있다.
// 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())
}
}
my action example
store
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const ADD_COUNT = 'ADD_COUNT';
const store = new Vuex.Store({
state: {
count: 100
},
mutations: {
[ADD_COUNT](state, payload) {
state.count += payload.num;
}
},
actions: {
incrementAsync({ commit }) {
return new Promise(resolve => {
setTimeout(() => {
commit(ADD_COUNT, { num: 300 });
resolve();
}, 1000);
});
}
}
});
export default store;
component : mapActions
로 액션을 method로 만든 부분을 확인하기!
<template>
<header id="header">
<button @click="addToCount({ num: 10 })">click here</button>
<button @click="incrementAsync300">async added number</button>
store : {{ storeCount }}
</header>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
export default {
data() {
return {
title: "Lillie's Trello",
count: 'this is local count'
};
},
computed: {
// ...mapState(['count']), // 원래는 유효하지만, 이 예제에서는 local state의 count와 이름이 겹쳐서 안됨.
...mapState({
// count: state => state.count,
// 원래는 유효하지만, 이 예제에서는 local state의 count와 이름이 겹쳐서 안됨.
storeCount: 'count', // string으로 넘기면, state => state.count와 같다.
sayLocalCount(state) {
// hi this is local count, this is count from store : 100
return `hi ${this.count}, this is count from store : ${state.count}`;
}
}),
myCount() {
return this.$store.getters.plusCount(10);
},
...mapGetters(['minusCount']),
...mapGetters({
mdCount: 'minusDoubleCount'
})
},
methods: {
...mapMutations({ addToCount: 'ADD_COUNT' }),
...mapActions({ incrementAsync300: 'incrementAsync' })
}
};
</script>