|
1 | 1 | # アクション
|
2 | 2 |
|
3 |
| -アクションはミューテーションをディスパッチする機能です。アクションは非同期にすることができ、単一アクションは複数のミューテーションをディスパッチできます。 |
| 3 | +> Vuex のアクションは純粋な Flux の定義では実際には "アクションクリエーター (action creators)" ですが、その用語は有用さよりも混乱を生み出していると考えられます。 |
4 | 4 |
|
5 |
| -アクションは何かが起こるための意向を表しており、それを呼び出すコンポーネントから離れて詳細を抽象化します。コンポーネントが何かしたい場合アクション呼び出します。アクションはステート変化をもたらすため、コールバックまたは戻り値について心配する必要はなく、そしてステート変化は更新するコンポーネントの DOM をトリガします。コンポーネントは、アクションが実際に行われている方法から、完全に切り離されます。 |
| 5 | +アクションはミューテーションをディスパッチする関数です。慣習として、 Vuex のアクションは常に1番目の引数にストアのインスタンスを受け取ることが期待され、2番目以降にはオプショナルな追加の引数が続きます。 |
6 | 6 |
|
7 |
| -それゆえ、通常アクション内部のデータエンドポイントへの API 呼び出しを行い、そしてアクションを呼び出すコンポーネントの両方から非同期に詳細を隠し、さらにミューテーションはアクションによってトリガされます。 |
| 7 | +``` js |
| 8 | +// 最も単純なアクション |
| 9 | +function increment (store) { |
| 10 | + store.dispatch('INCREMENT') |
| 11 | +} |
8 | 12 |
|
9 |
| -> Vuex のアクションは純粋な Flux の定義では実際には "アクションクリエータ (action creators)" ですが、私はその用語は便利よりも混乱していると見ています。 |
| 13 | +// 追加の引数を持つアクション |
| 14 | +// ES2015 の引数分割束縛(argument destructuring)を使用しています |
| 15 | +function incrementBy ({ dispatch }, amount) { |
| 16 | + dispatch('INCREMENT', amount) |
| 17 | +} |
| 18 | +``` |
10 | 19 |
|
11 |
| -### 単純なアクション |
| 20 | +これは、なぜ単純に直接ミューテーションをディスパッチしないのかと、一見馬鹿げて見えるかもしれません。**ミューテーションは同期的でなければならない** というのを覚えていますか? アクションはそうではありません。アクションの中では **非同期** の操作をおこなうことができます。 |
12 | 21 |
|
13 |
| -アクションは単純に単一のミューテーションをトリガするのが一般的です。Vuex はそのようなアクションの定義するために省略記法を提供します: |
| 22 | +``` js |
| 23 | +function incrementAsync ({ dispatch }) { |
| 24 | + setTimeout(() => { |
| 25 | + dispatch('INCREMENT') |
| 26 | + }, 1000) |
| 27 | +} |
| 28 | +``` |
| 29 | + |
| 30 | +より実践的な例として、ショッピングカートをチェックアウトするアクションを挙げます。このアクションは **非同期な API の呼び出し** と、**複数のミューテーションのディスパッチ** をします。 |
14 | 31 |
|
15 | 32 | ``` js
|
16 |
| -const store = new Vuex.Store({ |
17 |
| - state: { |
18 |
| - count: 1 |
19 |
| - }, |
20 |
| - mutations: { |
21 |
| - INCREMENT (state, x) { |
22 |
| - state.count += x |
| 33 | +function checkout ({ dispatch, state }, products) { |
| 34 | + // 現在のカート内の商品を保存します |
| 35 | + const savedCartItems = [...state.cart.added] |
| 36 | + // チェックアウトのリクエストを送信し、 |
| 37 | + // 楽観的にカート内をクリアします |
| 38 | + dispatch(types.CHECKOUT_REQUEST) |
| 39 | + // shop API は成功時のコールバックと失敗時のコールバックを受け取ります |
| 40 | + shop.buyProducts( |
| 41 | + products, |
| 42 | + // 成功処理 |
| 43 | + () => dispatch(types.CHECKOUT_SUCCESS), |
| 44 | + // 失敗処理 |
| 45 | + () => dispatch(types.CHECKOUT_FAILURE, savedCartItems) |
| 46 | + ) |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +非同期 API 呼び出しの結果は、アクションの返り値やコールバックから受け取るのではなく、同様にミューテーションをディスパッチすることで扱っていることに注意してください。これは **アクションの呼び出しによって生み出される唯一の副作用はミューテーションのディスパッチであるべき** だという経験則です。 |
| 51 | + |
| 52 | +### コンポーネント内でのアクション呼び出し |
| 53 | + |
| 54 | +アクション関数はストアのインスタンスへの参照無しに直接呼び出すことができないということに気づいているかもしれません。技術的に、メソッド内で `action(this.$store)` とアクションを呼び出すことはできます。しかし、ストアを "束縛した" 版のアクションをコンポーネントのメソッドとして呼び出すことができればより良いですし、テンプレートの中から簡単に参照することができるようになります。これは `vuex.actions` オプションを使うことで実現できます。 |
| 55 | + |
| 56 | +``` js |
| 57 | +// コンポーネント内 |
| 58 | +import { incrementBy } from './actions' |
| 59 | + |
| 60 | +const vm = new Vue({ |
| 61 | + vuex: { |
| 62 | + getters: { ... }, // ステートのゲッター |
| 63 | + actions: { |
| 64 | + incrementBy // ES6 オブジェクトリテラル省略記法(object literal shorthand)、同じ名前で束縛します |
23 | 65 | }
|
24 |
| - }, |
25 |
| - actions: { |
26 |
| - // 省略記法 |
27 |
| - // ミューテーション名を提供する |
28 |
| - increment: 'INCREMENT' |
29 | 66 | }
|
30 | 67 | })
|
31 | 68 | ```
|
32 | 69 |
|
33 |
| -今、アクションを呼び出すとき: |
| 70 | +上記のコードは元の `incrementBy` アクションをコンポーネントのストアインスタンスへと束縛し、それをコンポーネントのインスタンスメソッド `vm.incrementBy` として追加しています。`vm.incrementBy` に与えられた任意の引数は、第1引数にストアが渡されている状態で元のアクション関数へと渡されます。つまり以下のように呼ばれます。 |
34 | 71 |
|
35 | 72 | ``` js
|
36 |
| -store.actions.increment(1) |
| 73 | +vm.incrementBy(1) |
37 | 74 | ```
|
38 | 75 |
|
39 |
| -単純に私たちに対して以下を呼び出します: |
| 76 | +これは以下と同様です。 |
40 | 77 |
|
41 | 78 | ``` js
|
42 |
| -store.dispatch('INCREMENT', 1) |
| 79 | +incrementBy(vm.$store, 1) |
43 | 80 | ```
|
44 | 81 |
|
45 |
| -アクションに渡される任意の引数は、ミューテーションハンドラに渡されることに注意してください。 |
| 82 | +このようにする利点はコンポーネントのテンプレートの中でより簡単にそれを束縛することができるという点です。 |
46 | 83 |
|
47 |
| -### 標準なアクション |
| 84 | +``` html |
| 85 | +<button v-on:click="incrementBy(1)">1増加する</button> |
| 86 | +``` |
48 | 87 |
|
49 |
| -現在のステートに依存しているロジック、または非同期な操作を必要とするアクションについては、それらを関数として定義します。アクション関数は常に第1引数として呼び出す store を取得します: |
| 88 | +アクションを束縛するときに、明示的に異なるメソッド名を使うこともできます。 |
50 | 89 |
|
51 | 90 | ``` js
|
52 |
| -const vuex = new Vuex({ |
53 |
| - state: { |
54 |
| - count: 1 |
55 |
| - }, |
56 |
| - mutations: { |
57 |
| - INCREMENT (state, x) { |
58 |
| - state += x |
59 |
| - } |
60 |
| - }, |
61 |
| - actions: { |
62 |
| - incrementIfOdd: (store, x) => { |
63 |
| - if ((store.state.count + 1) % 2 === 0) { |
64 |
| - store.dispatch('INCREMENT', x) |
65 |
| - } |
| 91 | +// コンポーネント内 |
| 92 | +import { incrementBy } from './actions' |
| 93 | + |
| 94 | +const vm = new Vue({ |
| 95 | + vuex: { |
| 96 | + getters: { ... }, |
| 97 | + actions: { |
| 98 | + plus: incrementBy // 異なる名前で束縛します |
66 | 99 | }
|
67 | 100 | }
|
68 | 101 | })
|
69 | 102 | ```
|
70 | 103 |
|
| 104 | +このようにすることで、アクションは `vm.incrementBy` ではなく、 `vm.plus` と束縛されるでしょう。 |
| 105 | + |
| 106 | +### インラインアクション |
71 | 107 |
|
72 |
| -関数本体それほど冗長にしない ES6 の argument destructuring を使用するのが一般的です(ここでは、`dispatch` 関数は store インスタンスに事前にバインドされているように、それをメソッドとして呼び出す必要はありません): |
| 108 | +もしアクションがコンポーネント特有のものであれば、それを単純にインラインで定義することもできます。 |
73 | 109 |
|
74 | 110 | ``` js
|
75 |
| -// ... |
76 |
| -actions: { |
77 |
| - incrementIfOdd: ({ dispatch, state }, x) => { |
78 |
| - if ((state.count + 1) % 2 === 0) { |
79 |
| - dispatch('INCREMENT', x) |
| 111 | +const vm = new Vue({ |
| 112 | + vuex: { |
| 113 | + getters: { ... }, |
| 114 | + actions: { |
| 115 | + plus: ({ dispatch }) => dispatch('INCREMENT') |
80 | 116 | }
|
81 | 117 | }
|
82 |
| -} |
| 118 | +}) |
83 | 119 | ```
|
84 | 120 |
|
85 |
| -以下のように、文字列省略記法は基本的に糖衣構文 (syntax sugar) です: |
| 121 | +### すべてのアクションの束縛 |
| 122 | + |
| 123 | +もし、単純にすべての共通のアクションを束縛したいのであれば、以下のように書くことができます。 |
86 | 124 |
|
87 | 125 | ``` js
|
88 |
| -actions: { |
89 |
| - increment: 'INCREMENT' |
90 |
| -} |
91 |
| -// 以下に相当 ... : |
92 |
| -actions: { |
93 |
| - increment: ({ dispatch }, ...payload) => { |
94 |
| - dispatch('INCREMENT', ...payload) |
| 126 | +import * as actions from './actions' |
| 127 | + |
| 128 | +const vm = new Vue({ |
| 129 | + vuex: { |
| 130 | + getters: { ... }, |
| 131 | + actions // すべてのアクションを束縛 |
95 | 132 | }
|
96 |
| -} |
| 133 | +}) |
97 | 134 | ```
|
98 | 135 |
|
99 |
| -### 非同期なアクション |
| 136 | +### モジュール内でアクションをアレンジする |
100 | 137 |
|
101 |
| -非同期なアクションの定義に対して同じ構文を使用することができます: |
| 138 | +大抵の大きなアプリケーションでは、アクションは様々な目的にあわせてグループやモジュール内でアレンジされるべきでしょう。例えば、userActions モジュールはユーザーの登録、ログイン、ログアウトなどの処理を行い、その一方で、shoppingCartActions モジュールは買い物のためのその他のタスクを処理します。 |
102 | 139 |
|
103 |
| -``` js |
104 |
| -// ... |
105 |
| -actions: { |
106 |
| - incrementAsync: ({ dispatch }, x) => { |
107 |
| - setTimeout(() => { |
108 |
| - dispatch('INCREMENT', x) |
109 |
| - }, 1000) |
| 140 | +モジュール化をすることで、様々なコンポーネントで必要とされている最小限のアクションのインポートがよりしやすくなります。 |
| 141 | + |
| 142 | +再利用のためにあるアクションモジュールを別のアクションモジュールにインポートする場合もあるかもしれません。 |
| 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 | +``` |
| 156 | + |
| 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}) |
| 171 | + }) |
110 | 172 | }
|
111 | 173 | }
|
| 174 | + |
112 | 175 | ```
|
113 | 176 |
|
114 |
| -より実践的な例はショッピングカートをチェックアウトする場合です。複数のミューテーションをトリガする必要がある場合があります。チェックアウトを開始されたとき、成功、そして失敗の例を示します: |
| 177 | +アクションを別のモジュールから呼び出す時や、同一モジュール内の別のアクションを呼び出す時は、アクションが第1引数にストアのインスタンスをとることを覚えておきましょう。すなわち、アクション内で呼び出されるアクションには、呼び出し元が受け取った第1引数をそのまま渡すべきです。 |
115 | 178 |
|
116 |
| -``` js |
117 |
| -// ... |
118 |
| -actions: { |
119 |
| - checkout: ({ dispatch, state }, products) => { |
120 |
| - // カートアイテムで現在のアイテムを保存する |
121 |
| - const savedCartItems = [...state.cart.added] |
122 |
| - // チェックアウトリクエストを送り出し、 |
123 |
| - // 楽観的にカートをクリアします |
124 |
| - dispatch(types.CHECKOUT_REQUEST) |
125 |
| - // shop API は成功コールバックと失敗コールバックを受け入れます |
126 |
| - shop.buyProducts( |
127 |
| - products, |
128 |
| - // 成功処理 |
129 |
| - () => dispatch(types.CHECKOUT_SUCCESS), |
130 |
| - // 失敗処理 |
131 |
| - () => dispatch(types.CHECKOUT_FAILURE, savedCartItems) |
132 |
| - ) |
133 |
| - } |
| 179 | +もしアクションを ES6 の分割束縛(destructuring)スタイルで書いているのであれば、呼び出し元のアクションの第1引数は、両方のアクションが必要とするすべてのプロパティ、および、メソッドをカバーする必要があります。例えば、呼び出し元のアクションが *dispatch* のみを使用し、呼び出し先のアクションが *state* と *watch* を使用している時、呼び出し元の第1引数には、以下のように *dispatch*, *state*, *watch* のすべてを渡すべきです。 |
| 180 | + |
| 181 | +```javascript |
| 182 | +import {callee} from './anotherActionModule' |
| 183 | + |
| 184 | +export const caller = ({dispatch, state, watch}) => { |
| 185 | + dispatch('MUTATION_1') |
| 186 | + callee({state, watch}) |
134 | 187 | }
|
135 | 188 | ```
|
136 | 189 |
|
137 |
| -また、全てのコンポーネントは全体のチェックアウトを行うために `store.actions.checkout(products)` を呼び出す必要があります。 |
| 190 | +そうでなければ、旧式の関数の書き方をするべきです。 |
| 191 | + |
| 192 | +```javascript |
| 193 | +import {callee} from './anotherActionModule' |
| 194 | + |
| 195 | +export const caller = (store) => { |
| 196 | + store.dispatch('MUTATION_1') |
| 197 | + callee(store) |
| 198 | +} |
| 199 | +``` |
0 commit comments