8000 docs: actions · vuejs-ar/ar.vuejs.org@f2938cb · GitHub
[go: up one dir, main page]

Skip to content

Commit f2938cb

Browse files
committed
docs: actions
1 parent 9cb739e commit f2938cb

File tree

2 files changed

+137
-141
lines changed

2 files changed

+137
-141
lines changed

docs/en/actions.md

Lines changed: 117 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,199 +1,175 @@
11
# Actions
22

3-
> Vuex actions are in fact "action creators" in vanilla flux definitions, but I find that term more confusing than useful.
3+
Actions are similar to mutations, the difference being that:
44

5-
Actions are just functions that dispatch mutations. By convention, Vuex actions always expect a store instance as its first argument, followed by optional additional arguments:
5+
- Instead of mutating the state, actions commits mutations.
6+
- Actions can contain arbitrary asynchronous operations.
67

7-
``` js
8-
// the simplest action
9-
function increment (store) {
10-
store.dispatch('INCREMENT')
11-
}
8+
Let's register a simple action:
129

13-
// a action with additional arguments
14-
// with ES2015 argument destructuring
15-
function incrementBy ({ dispatch }, amount) {
16-
dispatch('INCREMENT', amount)
17-
}
10+
``` js
11+
const store = new Vuex.Store({
12+
state: {
13+
count: 0
14+
},
15+
mutations: {
16+
increment (state) {
17+
state.count++
18+
}
19+
},
20+
actions: {
21+
increment (context) {
22+
context.commit('increment')
23+
}
24+
}
25+
})
1826
```
1927

20-
This may look dumb at first sight: why don't we just dispatch mutations directly? Well, remember that **mutations must be synchronous**? Actions don't. We can perform **asynchronous** operations inside an action:
28+
Action handlers receive a context object which exposes the same set of methods/properties on the store instance, so you can call `ctx.commit` to commit a mutation, or access the state and getters via `ctx.state` and `ctx.getters`. We will see why this context object is not the store instance itself when we introduce [Modules](modules.md) later.
29+
30+
In practice, we often use ES2015 [argument destructuring](https://github.com/lukehoban/es6features#destructuring) to simplify the code a bit (especially when we need to call `commit` multiple times):
2131

2232
``` js
23-
function incrementAsync ({ dispatch }) {
24-
setTimeout(() => {
25-
dispatch('INCREMENT')
26-
}, 1000)
33+
actions: {
34+
increment ({ commit }) {
35+
commit('increment')
36+
}
2737
}
2838
```
2939

30-
A more practical example would be an action to checkout a shopping cart, which involves **calling an async API** and **dispatching multiple mutations**:
40+
### Dispatching Actions
41+
42+
Actions are triggered with the `store.dispatch` method:
3143

3244
``` js
33-
function checkout ({ dispatch, state }, products) {
34-
// save the current in cart items
35-
const savedCartItems = [...state.cart.added]
36-
// send out checkout request, and optimistically
37-
// clear the cart
38-
dispatch(types.CHECKOUT_REQUEST)
39-
// the shop API accepts a success callback and a failure callback
40-
shop.buyProducts(
41-
products,
42-
// handle success
43-
() => dispatch(types.CHECKOUT_SUCCESS),
44-
// handle failure
45-
() => dispatch(types.CHECKOUT_FAILURE, savedCartItems)
46-
)
47-
}
45+
store.dispatch('increment')
4846
```
4947

50-
Note that instead of expecting returns values or passing callbacks to actions, the result of calling the async API is handled by dispatching mutations as well. The rule of thumb is that **the only side effects produced by calling actions should be dispatched mutations**.
51-
52-
### Calling Actions In Components
53-
54-
You may have noticed that action functions are not directly callable without reference to a store instance. Technically, we can invoke an action by calling `action(this.$store)` inside a method, but it's better if we can directly expose "bound" versions of actions as the component's methods so that we can easily refer to them inside templates. We can do that using the `vuex.actions` option:
48+
This may look dumb at first sight: if we want to increment the count, why don't we just call `store.commit('increment')` directly? Well, remember that **mutations must be synchronous**? Actions don't. We can perform **asynchronous** operations inside an action:
5549

5650
``` js
57-
// inside a component
58-
import { incrementBy } from './actions'
59-
60-
const vm = new Vue({
61-
vuex: {
62-
getters: { ... }, // state getters
63-
actions: {
64-
incrementBy // ES6 object literal shorthand, bind using the same name
65-
}
51+
actions: {
52+
incrementAsync ({ commit }) {
53+
setTimeout(() => {
54+
commit('increment')
55+
})
6656
}
67-
})
57+
}
6858
```
6959

70-
What the above code does is to bind the raw `incrementBy` action to the component's store instance, and expose it on the component as an instance method, `vm.incrementBy`. Any arguments passed to `vm.incrementBy` will be passed to the raw action function after the first argument which is the store, so calling:
60+
Actions support the same payload format and object-style dispatch:
7161

7262
``` js
73-
vm.incrementBy(1)
63+
// dispatch with a payload
64+
store.dispatch('incrementAsync', {
65+
amount: 10
66+
})
67+
68+
// dispatch with an object
69+
store.dispatch({
70+
type: 'incrementAsync',
71+
amount: 10
72+
})
7473
```
7574

76-
is equivalent to:
75+
A more practical example of real-world actions would be an action to checkout a shopping cart, which involves **calling an async API** and **committing multiple mutations**:
7776

7877
``` js
79-
incrementBy(vm.$store, 1)
78+
actions: {
79+
checkout ({ commit, state }, payload) {
80+
// save the current in cart items
81+
const savedCartItems = [...state.cart.added]
82+
// send out checkout request, and optimistically
83+
// clear the cart
84+
commit(types.CHECKOUT_REQUEST)
85+
// the shop API accepts a success callback and a failure callback
86+
shop.buyProducts(
87+
products,
88+
// handle success
89+
() => commit(types.CHECKOUT_SUCCESS),
90+
// handle failure
91+
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
92+
)
93+
}
94+
}
8095
```
8196

82-
But the benefit is that we can bind to it more easily inside the component's template:
97+
Note we are performing a flow of asynchronous operations, and recording the side effects (state mutations) of the action by committing them.
8398

84-
``` html
85-
<button v-on:click="incrementBy(1)">increment by one</button>
86-
```
99+
### Dispatching Actions in Components
87100

88-
You can obviously use a different method name when binding actions:
101+
You can dispatch actions in components with `this.$store.dispatch('xxx')`, or use the `mapActions` helper which maps component methods to `store.dispatch` calls (requires root `store` injection):
89102

90103
``` js
91-
// inside a component
92-
import { incrementBy } from './actions'
93-
94-
const vm = new Vue({
95-
vuex: {
96-
getters: { ... },
97-
actions: {
98-
plus: incrementBy // bind using a different name
99-
}
104+
import { mapActions } from 'vuex'
105+
106+
export default {
107+
// ...
108+
methods: {
109+
...mapActions([
110+
'increment' // map this.increment() to this.$store.dispatch('increment')
111+
]),
112+
...mapActions({
113+
add: 'increment' // map this.add() to this.$store.dispatch('increment')
114+
})
100115
}
101-
})
116+
}
102117
```
103118

104-
Now the action will be bound as `vm.plus` instead of `vm.incrementBy`.
119+
### Composing Actions
105120

106-
### Inline Actions
121+
Actions are often asynchronous, so how do we know when an action is done? And more importantly, how can we compose multiple actions together to handle more complex async flows?
107122

108-
If an action is specific to a component, you can take the shortcut and just define it inline:
123+
The first thing to know is that `store.dispatch` returns the value returned by the triggered action handler, so you can return a Promise in an action:
109124

110125
``` js
111-
const vm = new Vue({
112-
vuex: {
113-
getters: { ... },
114-
actions: {
115-
plus: ({ dispatch }) => dispatch('INCREMENT')
116-
}
126+
actions: {
127+
actionA ({ commit }) {
128+
return new Promise((resolve, reject) => {
129+
setTimeout(() => {
130+
commit('someMutation')
131+
resolve()
132+
})
133+
})
117134
}
118-
})
135+
}
119136
```
120137

121-
### Binding All Actions
122-
123-
If you simply want to bind all the shared actions:
138+
Now you can do:
124139

125140
``` js
126-
import * as actions from './actions'
127-
128-
const vm = new Vue({
129-
vuex: {
130-
getters: { ... },
131-
actions // bind all actions
132-
}
141+
store.dispatch('actionA').then(() => {
142+
// ...
133143
})
134144
```
135145

136-
### Arrange Actions in Modules
137-
138-
Normally in large applications, actions should be arranged in groups/modules for different purposes. For example, userActions module deals with user registration, login, logout, and so on, while shoppingCartActions module deals with other tasks for shopping.
139-
140-
Modularization is more convenient for different components to import only required actions.
141-
142-
You may import action module into action module for reusability.
143-
144-
```javascript
145-
// errorActions.js
146-
export const setError = ({dispatch}, error) => {
147-
dispatch('SET_ERROR', error)
148-
}
149-
export const showError = ({dispatch}) => {
150-
dispatch('SET_ERROR_VISIBLE', true)
151-
}
152-
export const hideError = ({dispatch}) => {
153-
dispatch('SET_ERROR_VISIBLE', false)
154-
}
155-
```
146+
And also in another action:
156147

157-
```javascript
158-
// userActions.js
159-
import {setError, showError} from './errorActions'
160-
161-
export const login = ({dispatch}, username, password) => {
162-
if (username && password) {
163-
doLogin(username, password).done(res => {
164-
dispatch('SET_USERNAME', res.username)
165-
dispatch('SET_LOGGED_IN', true)
166-
dispatch('SET_USER_INFO', res)
167-
}).fail(error => {
168-
dispatch('SET_INVALID_LOGIN')
169-
setError({dispatch}, error)
170-
showError({dispatch})
148+
``` js
149+
actions: {
150+
// ...
151+
actionB ({ dispatch, commit }) {
152+
return dispatch('actionA').then(() => {
153+
commit('someOtherMutation')
171154
})
172155
}
173156
}
174-
175157
```
176158

177-
While calling actions from another module, or while calling another action in the same module, remember that actions take a store instance as its first argument, so the action called inside another action should be passed through the first argument for the caller.
178-
179-
If you write the action with ES6 destructuring style, make sure that the first argument of the caller action covers all the properties and methods of both actions. For example, only *dispatch* is used in the caller action and *state*, *watch* are used in the called action, all the *dispatch*, *state* and *watch* should be presented in the caller first formal argument like this:
180-
181-
```javascript
182-
import {callee} from './anotherActionModule'
159+
Finally, if we make use of [async / await](https://tc39.github.io/ecmascript-asyncawait/), a JavaScript feature landing very soon, we can compose our actions like this:
183160

184-
export const caller = ({dispatch, state, watch}) => {
185-
dispatch('MUTATION_1')
186-
callee({state, watch})
161+
``` js
162+
// assuming getData() and getOtherData() return Promises
163+
164+
actions: {
165+
async actionA ({ commit }) {
166+
commit('gotData', await getData())
167+
},
168+
async actionB ({ dispatch, commit }) {
169+
await dispatch('actionA') // wait for actionA to finish
170+
commit('gotOtherData', await getOtherData())
171+
}
187172
}
188173
```
189174

190-
Otherwise, you should use the old-fashioned function syntax:
191-
192-
```javascript
193-
import {callee} from './anotherActionModule'
194-
195-
export const caller = (store) => {
196-
store.dispatch('MUTATION_1')
197-
callee(store)
198-
}
199-
```
175+
> It's possible for a `store.dispatch` to trigger multiple action handlers in different modules. In such a case the returned value will be a Promise that resolves when all triggered handlers have been resolved.

docs/en/mutations.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,26 @@ mutations: {
153153

154154
Now imagine we are debugging the app and looking at the devtool's mutation logs. For every mutation logged, the devtool will need to capture a "before" and "after" snapshots of the state. However, the asynchronous callback inside the example mutation above makes that impossible: the callback is not called yet when the mutation is committed, and there's no way for the devtool to know when the callback will actually be called - any state mutation performed in the callback is essentially un-trackable!
155155

156+
### Commiting Mutations in Components
157+
158+
You can commit mutations in components with `this.$store.commit('xxx')`, or use the `mapMutations` helper which maps component methods to `store.commit` calls (requires root `store` injection):
159+
160+
``` js
161+
import { mapMutations } from 'vuex'
162+
163+
export default {
164+
// ...
165+
methods: {
166+
...mapMutations([
167+
'increment' // map this.increment() to this.$store.commit('increment')
168+
]),
169+
...mapMutations({
170+
add: 'increment' // map this.add() to this.$store.commit('increment')
171+
})
172+
}
173+
}
174+
```
175+
156176
### On to Actions
157177

158178
Asynchronicity combined with state mutation can make your program very hard to reason about. For example, when you call two methods both with async callbacks that mutate the state, how do you know when they are called and which callback was called first? This is exactly why we want to separate the two concepts. In Vuex, **mutations are synchronous transactions**:

0 commit comments

Comments
 (0)
0