diff --git a/CHANGELOG.md b/CHANGELOG.md index 9011d7f62..9d61dd62b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [4.0.2](https://github.com/vuejs/vuex/compare/v4.0.1...v4.0.2) (2021-06-17) + +### Bug Fixes + +* **devtools:** fix no getters displayed on root module + better getters inspector ([#1986](https://github.com/vuejs/vuex/issues/1986)) ([bc20295](https://github.com/vuejs/vuex/commit/bc20295331eb2bee40d6ae779d1ada31c542604c)) +* **build:** cjs build failing due to `__VUE_PROD_DEVTOOLS__` defined ([#1991](https://github.com/vuejs/vuex/issues/1991)) ([#1992](https://github.com/vuejs/vuex/issues/1992)) ([7151622](https://github.com/vuejs/vuex/commit/7151622d646968686546f1c4c80f7575c9b99176)) + ## [4.0.1](https://github.com/vuejs/vuex/compare/v4.0.0...v4.0.1) (2021-05-24) ### Features diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index dbfc8a059..300264a62 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -14,6 +14,11 @@ module.exports = { title: 'Vuex', description: 'Vue.js 的中心化状态管理方案' }, + '/ja/': { + lang: 'ja', + title: 'Vuex', + description: 'Vue.js のための集中状態管理' + }, '/ptbr/': { lang: 'pt-BR', title: 'Vuex', @@ -154,6 +159,65 @@ module.exports = { ] }, + '/ja/': { + label: '日本語', + selectText: '言語', + editLinkText: 'GitHub 上でこのページを編集する', + lastUpdated: '最終更新日時', + + nav: [ + { text: 'ガイド', link: '/ja/guide/' }, + { text: 'API リファレンス', link: '/ja/api/' }, + { text: 'リリースノート', link: 'https://github.com/vuejs/vuex/releases' }, + { + text: 'v4.x', + items: [ + { text: 'v3.x', link: 'https://vuex.vuejs.org/ja' } + ] + } + ], + + sidebar: [ + { + text: 'はじめに', + children: [ + { text: 'Vuex とは何か?', link: '/ja/' }, + { text: 'インストール', link: '/ja/installation' }, + { text: 'Vuex 入門', link: '/ja/guide/' } + ] + }, + { + text: 'コアコンセプト', + children: [ + { text: 'ステート', link: '/ja/guide/state' }, + { text: 'ゲッター', link: '/ja/guide/getters' }, + { text: 'ミューテーション', link: '/ja/guide/mutations' }, + { text: 'アクション', link: '/ja/guide/actions' }, + { text: 'モジュール', link: '/ja/guide/modules' } + ] + }, + { + text: '高度な活用', + children: [ + { text: 'アプリケーションの構造', link: '/ja/guide/structure' }, + { text: 'Composition API', link: '/ja/guide/composition-api' }, + { text: 'プラグイン', link: '/ja/guide/plugins' }, + { text: '厳格モード', link: '/ja/guide/strict' }, + { text: 'フォームの扱い', link: '/ja/guide/forms' }, + { text: 'テスト', link: '/ja/guide/testing' }, + { text: 'ホットリローディング', link: '/ja/guide/hot-reload' }, + { text: 'TypeScript サポート', link: '/ja/guide/typescript-support' }, + ] + }, + { + text: '移行 ガイド', + children: [ + { text: '3.x から 4.0 への移行', link: '/ja/guide/migrating-to-4-0-from-3-x' } + ] + } + ] + }, + '/ptbr/': { label: 'Português', selectText: 'Idiomas', diff --git a/docs/guide/getters.md b/docs/guide/getters.md index 058699fd7..f2242af88 100644 --- a/docs/guide/getters.md +++ b/docs/guide/getters.md @@ -20,6 +20,10 @@ Vuex allows us to define "getters" in the store. You can think of them as comput As of Vue 3.0, the getter's result is **not cached** as the computed property does. This is a known issue that requires Vue 3.1 to be released. You can learn more at [PR #1878](https://github.com/vuejs/vuex/pull/1883). ::: +::: warning WARNING +As of Vue 3.0, the getter's result is **not cached** as the computed property does. This is a known issue that requires Vue 3.2 to be released. You can learn more at [PR #1883](https://github.com/vuejs/vuex/pull/1883). +::: + Getters will receive the state as their 1st argument: ``` js diff --git a/docs/ja/api/index.md b/docs/ja/api/index.md new file mode 100644 index 000000000..7ce109579 --- /dev/null +++ b/docs/ja/api/index.md @@ -0,0 +1,390 @@ +--- +sidebar: auto +--- + +# API リファレンス + +## Store + +### createStore + +- `createStore(options: StoreOptions): Store` + + 新しいストアを作成します。 + + ```js + import { createStore } from 'vuex' + + const store = createStore({ ...options }) + ``` + +## Store コンストラクタオプション + +### state + +- 型: `Object | Function` + ストアのための ルートステートオブジェクトです。 [詳細](../guide/state.md) + + オブジェクトを返す関数を渡す場合、返されたオブジェクトはルートステートとして使用されます。これは特にモジュールの再利用のためにステートオブジェクトを再利用する場合に便利です。[詳細](../guide/modules.md#モジュールの再利用) + +### mutations + +- 型: `{ [type: string]: Function }` + + ストアにミューテーションを登録します。ハンドラ関数は第 1 引数に `state` を常に受け取り(モジュール内で定義されていれば、モジュールのローカルステートを受け取り)、指定されていれば第 2 引数に `payload` を受け取ります。 + + [詳細](../guide/mutations.md) + +### actions + +- 型: `{ [type: string]: Function }` + + ストアにアクションを登録します。ハンドラ関数は次のプロパティを持つ `context` オブジェクトを受け取ります。: + + ```js + { + state, // `store.state` と同じか、モジュール内にあればローカルステート + rootState, // `store.state` と同じ。ただしモジュール内に限る + commit, // `store.commit` と同じ + dispatch, // `store.dispatch` と同じ + getters, // `store.getters` と同じか、モジュール内にあればローカルゲッター + rootGetters // `store.getters` と同じ。ただしモジュール内に限る + } + ``` + + そして、第 2 引数の `payload` があれば、それを受け取ります。 + + [詳細](../guide/actions.md) + +### getters + +- 型: `{ [key: string]: Function }` + + ストアにゲッターを登録します. ゲッター関数は次の引数を受け取ります: + + ``` + state, // モジュール内で定義されていればモジュールのローカルステート + getters // store.getters と同じ + ``` + + モジュールで定義されたときの仕様 + + ``` + state, // モジュールで定義された場合、モジュールのローカルステート + getters, // 現在のモジュールのモジュールのローカルゲッター + rootState, // グローバルステート + rootGetters // 全てのゲッター + ``` + + 登録されたゲッターは `store.getters` 上に公開されます。 + + [詳細](../guide/getters.md) + +### modules + +- 型: `Object` + + サブモジュールを含む次のような形式のオブジェクトはストアにマージされます。 + + ```js + { + key: { + state, + namespaced?, + mutations?, + actions?, + getters?, + modules? + }, + ... + } + ``` + + 各モジュールは、ルートオプションに似た `state` と `mutations` を含むことができます。モジュールの状態は、モジュールのキーを使って、ストアのルートステートに結合されます。モジュールのミューテーションとゲッターは、第 1 引数としてルートステートの代わりに、モジュールのローカルステートだけを受け取り、モジュールのアクションの `context.state` もローカルステートを指すようになります。 + + [詳細](../guide/modules.md) + +### plugins + +- 型: `Array` + + プラグイン関数の配列は、ストアに適用されます。このプラグインは、ストアだけを引数として受け取り、外部への永続化、ロギング、デバッギングのために、ミューテーションを監視するか、または、 websocket や observable のような外から渡されるデータのためにミューテーションをディスパッチします。 + + [詳細](../guide/plugins.md) + +### strict + +- 型: `boolean` +- デフォルト: `false` + + Vuex ストアを厳格モードにします。厳格モードでは、ミューテーションハンドラ以外で、 Vuex の状態の変更を行うと、エラーが投げられます。 + + [詳細](../guide/strict.md) + +### devtools + +- 型: `boolean` + + 特定の Vuex インスタンスに対して開発ツールをオン、またはオフにします。インスタンスに false を渡すと、開発ツールのプラグインを購読しないように Vuex ストアに伝えます。1 ページに複数のストアがある時に便利です。 + + ```js + { + devtools: false + } + ``` + +## Store インスタンスプロパティ + +### state + +- 型: `Object` + + ルートステート、読み取り専用です。 + +### getters + +- 型: `Object` + + 登録されているゲッターを公開します。読み取り専用です。 + +## Store インスタンスメソッド + +### commit + +- `commit(type: string, payload?: any, options?: Object)` +- `commit(mutation: Object, options?: Object)` + + ミューテーションをコミットします。`options` は[名前空間付きモジュール](../guide/modules.md#名前空間)で root なミューテーションにコミットできる `root: true` を持つことできます。[詳細](../guide/mutations.md) + +### dispatch + +- `dispatch(type: string, payload?: any, options?: Object): Promise` +- `dispatch(action: Object, options?: Object): Promise` + + アクションをディスパッチします。`options` は[名前空間付きモジュール](../guide/modules.md#名前空間)で root なアクションにディスパッチできる `root: true` を持つことできます。 すべてのトリガーされたアクションハンドラを解決するPromiseを返します。[詳細](../guide/actions.md) + +### replaceState + +- `replaceState(state: Object)` + + ストアのルートステートを置き換えます。これは、ステートのハイドレーションやタイムトラベルのためだけに利用すべきです。 + +### watch + +- `watch(fn: Function, callback: Function, options?: Object): Function` + + `fn`が返す値をリアクティブに監視し、値が変わった時にコールバックを呼びます。`fn`は最初の引数としてストアのステートを、2番目の引数としてゲッターを受け取ります。 [Vue の`vm.$watch`メソッド](https://jp.vuejs.org/v2/api/#watch)と同じオプションをオプションのオブジェクトとして受け付けます。 + + 監視を止める場合は、返された unwatch 関数を呼び出します。 + +### subscribe + +- `subscribe(handler: Function, options?: Object): Function` + + ストアへのミューテーションを購読します。`handler` は、全てのミューテーションの後に呼ばれ、引数として、ミューテーション ディスクリプタとミューテーション後の状態を受け取ります。 + + ```js + const unsubscribe = store.subscribe((mutation, state) => { + console.log(mutation.type) + console.log(mutation.payload) + }) + ``` + + デフォルトでは、新しい `handler` はチェーンの最後に登録されます。つまり、先に追加された他の `handler` が呼び出された後に実行されます。`prepend: true` を `options` に設定することで、`handler` をチェーンの最初に登録することができます。 + + ```js + store.subscribe(handler, { prepend: true }) + ``` + + 購読を停止するには、返された unsubscribe 関数呼び出します。 + + プラグインの中でもっともよく利用されます。[詳細](../guide/plugins.md) + +### subscribeAction + +- `subscribeAction(handler: Function, options?: Object): Function` + + スストアアクションを購読します。`handler` はディスパッチされたアクションごとに呼び出され、アクション記述子と現在のストア状態を引数として受け取ります: + + ```js + const unsubscribe = store.subscribeAction((action, state) => { + console.log(action.type) + console.log(action.payload) + }) + ``` + + デフォルトでは、新しい `handler` はチェーンの最後に登録されます。つまり、先に追加された他の `handler` が呼び出された後に実行されます。`prepend: true` を `options` に設定することで、`handler` をチェーンの最初に登録することができます。 + + ```js + store.subscribeAction(handler, { prepend: true }) + ``` + + 購読を停止するには、返された unsubscribe 関数を呼び出します。 + + `subscribeAction` は購読ハンドラがアクションディスパッチの*前 (before)*、または*後 (after)* に呼びだすべきかどうか(デフォルトの動作は、*before* です)指定することもできます。 + + ```js + store.subscribeAction({ + before: (action, state) => { + console.log(`before action ${action.type}`) + }, + after: (action, state) => { + console.log(`after action ${action.type}`) + } + }) + ``` + + `subscribeAction` は `error` ハンドラを指定することもできます。このハンドラは、アクションディスパッチの中で投げられたエラーをキャッチすることができます。`error` ハンドラは投げられた `error` オブジェクトを第 3 引数として受け取ります。 + + ```js + store.subscribeAction({ + error: (action, state, error) => { + console.log(`error action ${action.type}`) + console.error(error) + } + }) + ``` + + `subscribeAction` メソッドはプラグインで最も一般的に使用されます。[詳細](../guide/plugins.md) + +### registerModule + +- `registerModule(path: string | Array, module: Module, options?: Object)` + + 動的なモジュールを登録します。[詳細](../guide/modules.md#dynamic-module-registration) + + `options` は前の状態を保存する `preserveState: true` を持つことができます。サーバサイドレンダリングに役立ちます。 + +### unregisterModule + +- `unregisterModule(path: string | Array)` + + 動的なモジュールを解除します。[詳細](../guide/modules.md#dynamic-module-registration) + +### hasModule + +- `hasModule(path: string | Array): boolean` + + 動的なモジュールがすでに登録されているかどうかを確認します。[詳細](../guide/modules.md#dynamic-module-registration) + +### hotUpdate + +- `hotUpdate(newOptions: Object)` + + 新しいアクションとミューテーションをホットスワップします。[詳細](../guide/hot-reload.md) + +## コンポーネントをバインドするヘルパー + +### mapState + +- `mapState(namespace?: string, map: Array | Object): Object` + + ストアのサブツリーを返すコンポーネントの computed オプションを作成します。[詳細](../guide/state.md#the-mapstate-helper) + + 第 1 引数は、オプションで名前空間文字列にすることができます。[詳細](../guide/modules.md#binding-helpers-with-namespace) + + 第 2 引数のオブジェクトのメンバーには関数 `function(state: any)` を指定できます。 + +### mapGetters + +- `mapGetters(namespace?: string, map: Array | Object): Object` + + ゲッターの評価後の値を返すコンポーネントの computed オプションを作成します。[詳細](../guide/getters.md#the-mapgetters-helper) + + 第 1 引数は、オプションで名前空間文字列にすることができます。[詳細](../guide/modules.md#binding-helpers-with-namespace) + +### mapActions + +- `mapActions(namespace?: string, map: Array | Object): Object` + + アクションをディスパッチするコンポーネントの methods オプションを作成します。[詳細](../guide/actions.md#dispatching-actions-in-components) + + 第 1 引数は、オプションで名前空間文字列にすることができます。[詳細](../guide/modules.md#binding-helpers-with-namespace) + + 第 2 引数のオブジェクトのメンバーには関数 `function(dispatch: function, ...args: any[])` を指定できます。 + +### mapMutations + +- `mapMutations(namespace?: string, map: Array | Object): Object` + + ミューテーションをコミットするコンポーネントの methods オプションを作成します。[詳細](../guide/mutations.md#commiting-mutations-in-components) + + 第 1 引数は、オプションで名前空間文字列にすることができます。[詳細](../guide/modules.md#binding-helpers-with-namespace) + +  第 2 引数のオブジェクトのメンバーには関数 `function(commit: function, ...args: any[])` を指定できます。 + +### createNamespacedHelpers + +- `createNamespacedHelpers(namespace: string): Object` + + 名前空間付けられたコンポーネントバインディングのヘルパーを作成します。返されるオブジェクトは指定された名前空間にバインドされた `mapState`、`mapGetters`、`mapActions` そして `mapMutations` が含まれます。[詳細はこちら](../guide/modules.md#binding-helpers-with-namespace) + +## Composable 関数 + +### useStore + +- `useStore(injectKey?: InjectionKey> | string): Store;` + + `setup` フックの中で呼ばれた時に、Vue App インスタンスにインストールされたストアを取得します。Composition API を使用する時、このメソッドを呼ぶことでストアを取得することができます。 + + ```js + import { useStore } from 'vuex' + + export default { + setup () { + const store = useStore() + } + } + ``` + + TypeScript ユーザーは、 `InjectionKey` を使って型付けされたストアを取得することができます。そのためには、ストアインスタンスを Vue App インスタンスにインストールする時、`InjectionKey` を定義してストアと一緒に渡す必要があります。 + + まず、Vue の `InjectionKey` インターフェースを使って、 `InjectionKey` を宣言します。 + + ```ts + // store.ts + import { InjectionKey } from 'vue' + import { createStore, Store } from 'vuex' + + export interface State { + count: number + } + + export const key: InjectionKey> = Symbol() + + export const store = createStore({ + state: { + count: 0 + } + }) + ``` + + 次に、定義したキーを `app.use` メソッドの第2引数に渡します。 + + ```ts + // main.ts + import { createApp } from 'vue' + import { store, key } from './store' + + const app = createApp({ ... }) + + app.use(store, key) + + app.mount('#app') + ``` + + 最後に、`useStore` 関数にキーを渡すことで、型付けされたストアインスタンスを取得することができます。 + + ```ts + // vue component 内 + import { useStore } from 'vuex' + import { key } from './store' + + export default { + setup () { + const store = useStore(key) + + store.state.count // number として型付け + } + } + ``` diff --git a/docs/ja/guide/actions.md b/docs/ja/guide/actions.md new file mode 100644 index 000000000..1bbe4c456 --- /dev/null +++ b/docs/ja/guide/actions.md @@ -0,0 +1,179 @@ +# アクション + + + +アクションはミューテーションと似ていますが、下記の点で異なります: + +- アクションは、状態を変更するのではなく、ミューテーションをコミットします。 +- アクションは任意の非同期処理を含むことができます。 + +シンプルなアクションを登録してみましょう: + +``` js +const store = createStore({ + state: { + count: 0 + }, + mutations: { + increment (state) { + state.count++ + } + }, + actions: { + increment (context) { + context.commit('increment') + } + } +}) +``` + +アクションハンドラはストアインスタンスのメソッドやプロパティのセットと同じものを呼び出せるコンテキストオブジェクトを受け取ります。したがって `context.commit` を呼び出すことでミューテーションをコミットできます。あるいは `context.state` や `context.getters` で、状態やゲッターにアクセスできます。他のアクションも `context.dispatch` で呼ぶこともできます。なぜコンテキストオブジェクトがストアインスタンスそのものではないのかは、後ほど[モジュール](modules.md)で説明します。 + +実際にはコードを少しシンプルにするために ES2015 の[引数分割束縛(argument destructuring)](https://github.com/lukehoban/es6features#destructuring)がよく使われます(特に `commit` を複数回呼び出す必要があるとき): + +``` js +actions: { + increment ({ commit }) { + commit('increment') + } +} +``` + +## アクションのディスパッチ + +アクションは `store.dispatch` がトリガーとなって実行されます: + +``` js +store.dispatch('increment') +``` + +これは一見ばかげて見えるかもしれません。つまり、カウントをインクリメントしたいときに、どうして直接 `store.commit('increment')` を呼び出してミューテーションをコミットしないのか、と。**ミューテーションは同期的でなければならない**というのを覚えていますか?アクションはそうではありません。アクションの中では**非同期**の操作を行うことができます。 + +``` js +actions: { + incrementAsync ({ commit }) { + setTimeout(() => { + commit('increment') + }, 1000) + } +} +``` + +アクションはペイロード形式とオブジェクトスタイルのディスパッチをサポートします: + +``` js +// ペイロードを使ってディスパッチする +store.dispatch('incrementAsync', { + amount: 10 +}) + +// オブジェクトを使ってディスパッチする +store.dispatch({ + type: 'incrementAsync', + amount: 10 +}) +``` + +より実践的な例として、ショッピングカートをチェックアウトするアクションを挙げます。このアクションは**非同期な API の呼び出し**と、**複数のミューテーションのコミット**をします: + +``` js +actions: { + checkout ({ commit, state }, products) { + // 現在のカート内の商品を保存する + const savedCartItems = [...state.cart.added] + // チェックアウトのリクエストを送信し、楽観的にカート内をクリアする + commit(types.CHECKOUT_REQUEST) + // shop API は成功時のコールバックと失敗時のコールバックを受け取る + shop.buyProducts( + products, + // 成功時の処理 + () => commit(types.CHECKOUT_SUCCESS), + // 失敗時の処理 + () => commit(types.CHECKOUT_FAILURE, savedCartItems) + ) + } +} +``` + +一連の非同期の処理を実行しつつ、ミューテーションのコミットによってのみ副作用(状態の変更)を与えていることに注意してください。 + +## コンポーネント内でのアクションのディスパッチ + +`this.$store.dispatch('xxx')` でコンポーネント内でアクションをディスパッチできます。あるいはコンポーネントのメソッドを `store.dispatch` にマッピングする `mapActions` ヘルパーを使うこともできます(ルートの `store` の注入が必要です): + +``` js +import { mapActions } from 'vuex' + +export default { + // ... + methods: { + ...mapActions([ + 'increment', // `this.increment()` を `this.$store.dispatch('increment')` にマッピングする('increment')` + + // `mapActions` もペイロードをサポートする: + 'incrementBy' // `this.incrementBy(amount)` を `this.$store.dispatch('incrementBy', amount)` にマッピングする + ]), + ...mapActions({ + add: 'increment' // `this.add()` を `this.$store.dispatch('increment')` にマッピングする + }) + } +} +``` + +## アクションを構成する + +アクションはしばしば非同期処理を行いますが、アクションが完了したことをどうやって知れば良いのでしょう?そしてもっと重要なことは、さらに複雑な非同期処理を取り扱うために、どうやって複数のアクションを構成させるかということです。 + +まず知っておくべきことは `store.dispatch` がトリガーされたアクションハンドラによって返された Promise を処理できることと、`store.dispatch` もまた Promise を返すことです。 + +``` js +actions: { + actionA ({ commit }) { + return new Promise((resolve, reject) => { + setTimeout(() => { + commit('someMutation') + resolve() + }, 1000) + }) + } +} +``` + +すると次のようにできます: + +``` js +store.dispatch('actionA').then(() => { + // ... +}) +``` + +また別のアクションで下記のように書くと: + +``` js +actions: { + // ... + actionB ({ dispatch, commit }) { + return dispatch('actionA').then(() => { + commit('someOtherMutation') + }) + } +} +``` + +最終的に [async / await](https://tc39.github.io/ecmascript-asyncawait/) を使用することで、次のようにアクションを組み合わせることができます: + +``` js +// `getData()` と `getOtherData()` が Promise を返すことを想定している + +actions: { + async actionA ({ commit }) { + commit('gotData', await getData()) + }, + async actionB ({ dispatch, commit }) { + await dispatch('actionA') // `actionA` が完了するのを待機する + commit('gotOtherData', await getOtherData()) + } +} +``` + +> `store.dispatch` で異なるモジュール内の複数のアクションハンドラをトリガーすることができます。そのようなケースでは、全てのトリガーされたハンドラが解決されたときに解決する Promise が戻り値として返ってくることになります。 diff --git a/docs/ja/guide/composition-api.md b/docs/ja/guide/composition-api.md new file mode 100644 index 000000000..a7517e105 --- /dev/null +++ b/docs/ja/guide/composition-api.md @@ -0,0 +1,62 @@ +# Composition API + +`setup` フックの中でストアにアクセスするには、`useStore` 関数を呼び出します。これは、Option API を使って、コンポーネント内で `this.$store` を取得するのと同等です。 + +```js +import { useStore } from 'vuex' + +export default { + setup () { + const store = useStore() + } +} +``` + +## ステートとゲッターへのアクセス + +ステートやゲッターにアクセスするためには、リアクティビティを保持するために `computed` による参照を作成する必要があります。これは、Option API を使って、算出プロパティを作成するのと同じことです。 + +```js +import { computed } from 'vue' +import { useStore } from 'vuex' + +export default { + setup () { + const store = useStore() + + return { + // computed 関数のステートにアクセスします + count: computed(() => store.state.count), + + // computed 関数のゲッターにアクセスします + double: computed(() => store.getters.double) + } + } +} +``` + +## ミューテーションとアクションへのアクセス + +ミューテーションとアクションにアクセスするには、`setup` フック内で `commit` と `dispatch` 関数を呼び出します。 + +```js +import { useStore } from 'vuex' + +export default { + setup () { + const store = useStore() + + return { + // ミューテーションにアクセスする + increment: () => store.commit('increment'), + + // アクションにアクセスする + asyncIncrement: () => store.dispatch('asyncIncrement') + } + } +} +``` + +## Composition API での実装例 + +Vuex と Vue の Composition API を利用したアプリケーションの例は、[Composition API example](https://github.com/vuejs/vuex/tree/4.0/examples/composition) をご覧ください。 diff --git a/docs/ja/guide/forms.md b/docs/ja/guide/forms.md new file mode 100644 index 000000000..99aef59b9 --- /dev/null +++ b/docs/ja/guide/forms.md @@ -0,0 +1,61 @@ +# フォームの扱い + + + +厳格モードで Vuex を使用するとき、Vuex に属する状態の一部で `v-model` を使用するのは少しトリッキーです: + +``` html + +``` + +`obj` がストアからオブジェクトを返す算出プロパティ (computed property) と仮定すると、`v-model` は input でユーザーが入力するとき、直接 `obj.message` を変更します。厳格モードでは、この変更は明示的に Vuex のミューテーションハンドラ内部で処理されていないため、エラーを投げます。 + +それに対処するための "Vuex way" は、`` の値をバインディングし、`input` または `change` イベントでアクションを呼び出すことです: + +``` html + +``` +``` js +// ... +computed: { + ...mapState({ + message: state => state.obj.message + }) +}, +methods: { + updateMessage (e) { + this.$store.commit('updateMessage', e.target.value) + } +} +``` + +ミューテーションのハンドラは以下のようになります: + +``` js +// ... +mutations: { + updateMessage (state, message) { + state.obj.message = message + } +} +``` + +## 双方向算出プロパティ + +確かに、上記の例は単純な `v-model` と ローカルステートよりもかなり冗長で、`v-model` のいくつかの有用な機能が使えません。代わりに、セッターで双方向算出プロパティを使うアプローチがあります。 + +``` html + +``` +``` js +computed: { + message: { + get () { + return this.$store.state.obj.message + }, + set (value) { + this.$store.commit('updateMessage', value) + } + } +} +``` diff --git a/docs/ja/guide/getters.md b/docs/ja/guide/getters.md new file mode 100644 index 000000000..fa8e0fb80 --- /dev/null +++ b/docs/ja/guide/getters.md @@ -0,0 +1,121 @@ +# ゲッター + + + +例えば項目のリストをフィルタリングしたりカウントするときのように、ストアの状態を算出したいときがあります。 + +``` js +computed: { + doneTodosCount () { + return this.$store.state.todos.filter(todo => todo.done).length + } +} +``` + +もしこの関数を複数のコンポーネントで利用したくなったら、関数をコピーするか、あるいは関数を共用のヘルパーに切り出して複数の場所でインポートする必要があります。しかし、どちらも理想的とはいえません。 + +Vuex を利用するとストア内に "ゲッター" を定義することができます。それらをストアの算出プロパティと考えることができます。 + +::: warning 警告 +Vue 3.0 では、ゲッターの結果は算出プロパティのように**キャッシュされません**。これは既知の問題で、Vue 3.1 がリリースされる必要があります。詳細は [PR #1878](https://github.com/vuejs/vuex/pull/1883) をご確認ください。 +::: + +ゲッターは第1引数として、state を受け取ります: + +``` js +const store = createStore({ + state: { + todos: [ + { id: 1, text: '...', done: true }, + { id: 2, text: '...', done: false } + ] + }, + getters: { + doneTodos (state) { + return state.todos.filter(todo => todo.done) + } + } +}) +``` + +## プロパティスタイルアクセス + +ゲッターは `store.getters` オブジェクトから取り出され、プロパティとしてアクセスすることができます: + +``` js +store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }] +``` + +ゲッターは第2引数として他のゲッターを受け取ります: + +``` js +getters: { + // ... + doneTodosCount (state, getters) { + return getters.doneTodos.length + } +} +``` + +``` js +store.getters.doneTodosCount // -> 1 +``` + +どのコンポーネントの内部でも簡単にゲッターを利用することができます: + +``` js +computed: { + doneTodosCount () { + return this.$store.getters.doneTodosCount + } +} +``` + +プロパティとしてアクセスされるゲッターは Vue のリアクティブシステムの一部としてキャッシュされるという点に留意してください。 + +## メソッドスタイルアクセス + +関数を返り値にすることで、ゲッターに引数を渡すこともできます。これは特にストアの中の配列を検索する時に役立ちます: +```js +getters: { + // ... + getTodoById: (state) => (id) => { + return state.todos.find(todo => todo.id === id) + } +} +``` + +``` js +store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false } +``` + +メソッドによってアクセスされるゲッターは呼び出す度に実行され、その結果はキャッシュされない点に留意してください。 + +## `mapGetters` ヘルパー + +`mapGetters` ヘルパーはストアのゲッターをローカルの算出プロパティにマッピングさせます: + +``` js +import { mapGetters } from 'vuex' + +export default { + // ... + computed: { + // ゲッターを、スプレッド演算子(object spread operator)を使って computed に組み込む + ...mapGetters([ + 'doneTodosCount', + 'anotherGetter', + // ... + ]) + } +} +``` + +ゲッターを異なる名前でマッピングさせたいときはオブジェクトを使います: + +``` js +...mapGetters({ + // `this.doneCount` を `this.$store.getters.doneTodosCount` にマッピングさせる + doneCount: 'doneTodosCount' +}) +``` diff --git a/docs/ja/guide/hot-reload.md b/docs/ja/guide/hot-reload.md new file mode 100644 index 000000000..d1a31db13 --- /dev/null +++ b/docs/ja/guide/hot-reload.md @@ -0,0 +1,85 @@ +# ホットリローディング + +Vuex は webpack の [Hot Module Replacement API](https://webpack.js.org/guides/hot-module-replacement/) を使用することで、アプリケーションの開発を行っている間のミューテーション、モジュール、アクション、ゲッターのホットリローディングをサポートします。Browserify では [browserify-hmr](https://github.com/AgentME/browserify-hmr/) プラグインを使用することができます。 + +ミューテーションとモジュールのホットリローディングのために、`store.hotUpdate()` API メソッドを利用する必要があります: + +``` js +// store.js +import { createStore } from 'vuex' +import mutations from './mutations' +import moduleA from './modules/a' + +const state = { ... } + +const store = createStore({ + state, + mutations, + modules: { + a: moduleA + } +}) + +if (module.hot) { + // ホットモジュールとしてアクションとモジュールを受け付けます + module.hot.accept(['./mutations', './modules/a'], () => { + // 更新されたモジュールをインポートする + // babel 6 のモジュール出力のため、ここでは .default を追加しなければならない + const newMutations = require('./mutations').default + const newModuleA = require('./modules/a').default + // 新しいモジュールとミューテーションにスワップ + store.hotUpdate({ + mutations: newMutations, + modules: { + a: newModuleA + } + }) + }) +} +``` + +ホットリローディングを試したい場合は、[counter-hot example](https://github.com/vuejs/vuex/tree/dev/examples/counter-hot)をチェックアウトしてください。 + +## 動的モジュールホットリローディング + +もしストアでモジュールだけを使用している場合には、`require.context` を使って全てのモジュールを動的に読み込むこともできます。 + +```js +// store.js +import { createStore } from 'vuex' + +// 全てのモジュールをロードする +function loadModules() { + const context = require.context("./modules", false, /([a-z_]+)\.js$/i) + + const modules = context + .keys() + .map((key) => ({ key, name: key.match(/([a-z_]+)\.js$/i)[1] })) + .reduce( + (modules, { key, name }) => ({ + ...modules, + [name]: context(key).default + }), + {} + ) + + return { context, modules } +} + +const { context, modules } = loadModules() + +const store = createStore({ + modules +}) + +if (module.hot) { + // モジュールに変更があった場合にホットリロードする + module.hot.accept(context.id, () => { + const { modules } = loadModules() + + store.hotUpdate({ + modules + }) + }) +} +``` diff --git a/docs/ja/guide/index.md b/docs/ja/guide/index.md new file mode 100644 index 000000000..00563880b --- /dev/null +++ b/docs/ja/guide/index.md @@ -0,0 +1,66 @@ +# Vuex 入門 + + + +Vuex アプリケーションの中心にあるものは**ストア**です。"ストア" は、基本的にアプリケーションの **状態(state)** を保持するコンテナです。単純なグローバルオブジェクトとの違いが 2つあります。 + +1. Vuex ストアはリアクティブです。Vue コンポーネントがストアから状態を取り出すとき、もしストアの状態が変化したら、ストアはリアクティブかつ効率的に更新を行います。 + +2. ストアの状態を直接変更することはできません。明示的に**ミューテーションをコミットする**ことによってのみ、ストアの状態を変更します。これによって、全ての状態の変更について追跡可能な記録を残すことが保証され、ツールでのアプリケーションの動作の理解を助けます。 + +## シンプルなストア + +:::tip 注意 +私たちは、このドキュメントのコード例に ES2015 のシンタックスを利用しています。 もし触れたことがなければ、[ぜひ触れてください](https://babeljs.io/docs/learn-es2015/)! +::: + +Vuex を[インストール](../installation.md) してから、ストアをつくってみましょう。Vuex ストアの作成は、とても簡単です。ストアオブジェクトの初期状態と、いくつかのミューテーションを準備するだけです。 + +```js +import { createApp } from 'vue' +import { createStore } from 'vuex' + +// 新しいストアインスタンスを作成します +const store = createStore({ + state () { + return { + count: 0 + } + }, + mutations: { + increment (state) { + state.count++ + } + } +}) + +const app = createApp({ /* ルートコンポーネント */ }) + +// プラグインとしてストアインスタンスをインストールします +app.use(store) +``` + +これで `store.state` でストアオブジェクトの状態を参照でき、また `store.commit` メソッドで状態の変更を行うことができます。 + +```js +store.commit('increment') + +console.log(store.state.count) // -> 1 +``` + +Vue コンポーネントでは、`this.$store` としてストアにアクセスできます。それでは、コンポーネントのメソッドを使ってミューテーションをコミットしてみましょう。 + +```js +methods: { + increment() { + this.$store.commit('increment') + console.log(this.$store.state.count) + } +} +``` + +そして `store.state.count` を直接変更する代わりにミューテーションをコミットする理由は、状態の変更を明確に追跡したいからです。このシンプルな規約は、あなたのコードの意図をさらに明確にし、コードを読んだ時にアプリケーションの状態の変更について、論理的に考えることができるようにします。加えて、私たちに全ての変更のログを取ったり、状態のスナップショットを取ったり、タイムトラベルデバッグを行うようなツールを実装する余地を与えてくれます。 + +ストアオブジェクトの状態はリアクティブなので、ストアの状態をコンポーネント内で使うには算出プロパティ内でただ状態を返せば良いです。コンポーネントメソッドでミューテーションをコミットすることによって状態の変更を行います。 + +これから Vuex のコアコンセプトについて詳しく説明していきます。まずは[状態(state)](state.md)からはじめましょう。 diff --git a/docs/ja/guide/migrating-to-4-0-from-3-x.md b/docs/ja/guide/migrating-to-4-0-from-3-x.md new file mode 100644 index 000000000..3733b7e03 --- /dev/null +++ b/docs/ja/guide/migrating-to-4-0-from-3-x.md @@ -0,0 +1,116 @@ +# 3.x から 4.0 への移行 + +ほとんどすべての Vuex 4 の API は、Vuex 3 から変更されていません。しかし、修正が必要な破壊的変更がいくつかあります。 + +- [破壊的変更](#破壊的変更) + - [インストール手順](#インストール手順) + - [TypeScript サポート](#typescript-サポート) + - [バンドルが Vue 3 に対応しました](#バンドルが-vue-3-に対応しました) + - ["createLogger" 関数はコアモジュールからエクスポートされます](#createlogger-関数はコアモジュールからエクスポートされます) +- [新機能](#新機能) + - [新しい "useStore" 合成関数](#新しい-usestore-合成関数) + +## 破壊的変更 + +### インストール手順 + +新しい Vue 3 の初期化の手順に合わせて、Vuex のインストール手順が変更されました。新しいストアを作成するには、新しく導入された createStore 関数を使用することが推奨されます。 + +```js +import { createStore } from 'vuex' + +export const store = createStore({ + state () { + return { + count: 1 + } + } +}) +``` + +VueインスタンスにVuexをインストールするには、Vuexではなく`store`を渡します。 + +```js +import { createApp } from 'vue' +import { store } from './store' +import App from './App.vue' + +const app = createApp(App) + +app.use(store) + +app.mount('#app') +``` + +:::tip 注意 +厳密にはこれは破壊的変更ではなく、まだ `new Store(...)` 構文を使用することができますが、Vue 3 と Vue Router Next に合わせるためにこの方法を推奨します。 +::: + +### TypeScript サポート + +Vuex 4 は、[issue #994](https://github.com/vuejs/vuex/issues/994) を解決するために、Vue コンポーネント内の `this.$store` のグローバルな型付けを削除します。TypeScript で使用する場合は、独自のモジュール拡張を宣言する必要があります。 + +次のコードをあなたのプロジェクトに配置して、`this.$store` が正しく型付けされるようにしてください。 + +```ts +// vuex-shim.d.ts + +import { ComponentCustomProperties } from 'vue' +import { Store } from 'vuex' + +declare module '@vue/runtime-core' { + // ストアのステートを宣言する + interface State { + count: number + } + + interface ComponentCustomProperties { + $store: Store + } +} +``` + +詳細は、[TypeScript サポート](./typescript-support) セクションをご覧ください. + +### バンドルが Vue 3 に対応しました + +以下のバンドルは、Vue 3 のバンドルに合わせて生成されます。 + +- `vuex.global(.prod).js` + - ブラウザの ` + +``` + +## NPM + +```bash +npm install vuex@next --save +``` + +## Yarn + +```bash +yarn add vuex@next --save +``` + +## Promise + +Vuex は [Promise (プロミス)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) を必要とします。ブラウザで Promise が実装されていない(例 IE)場合は、[es6-promise](https://github.com/stefanpenner/es6-promise) のようなポリフィルライブラリを使用できます。 + +CDN 経由でそれを含めることができます: + +```html + +``` + +`window.Promise` は自動的に有効になります。 + +NPM または Yarn のようなパッケージマネージャーを使用するのを希望する場合は、以下のコマンドでインストールします: + +```bash +npm install es6-promise --save # NPM +yarn add es6-promise # Yarn +``` + +さらに、Vuex を使用する前に、コードのどこかに次の行を追加します: + +```js +import 'es6-promise/auto' +``` + +## 開発版ビルド + +最新の開発版ビルドを利用したい場合には、 GitHub から直接クローンし `vuex` を自身でビルドする必要があります。 + +```bash +git clone https://github.com/vuejs/vuex.git node_modules/vuex +cd node_modules/vuex +yarn +yarn build +``` diff --git a/docs/ptbr/guide/typescript-support.md b/docs/ptbr/guide/typescript-support.md index 49bd2c528..6fae7fd59 100644 --- a/docs/ptbr/guide/typescript-support.md +++ b/docs/ptbr/guide/typescript-support.md @@ -96,7 +96,7 @@ Por baixo dos panos, o Vuex instala o _store_ para a aplicação Vue usando o [P Ter que importar `InjectionKey` e passá-lo para `useStore` em todos os lugares em que é usado pode rapidamente se tornar uma tarefa repetitiva. Para simplificar as coisas, você pode definir sua própria função combinável (ou _composable_ _function_) para recuperar um _store_ tipado: ```ts -// store.js +// store.ts import { InjectionKey } from 'vue' import { createStore, useStore as baseUseStore, Store } from 'vuex' diff --git a/package.json b/package.json index ccca6380b..3aae30a88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vuex", - "version": "4.0.1", + "version": "4.0.2", "description": "state management for Vue.js", "main": "dist/vuex.cjs.js", "exports": { diff --git a/rollup.config.js b/rollup.config.js index 1ff829389..30b9df0bb 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -27,6 +27,7 @@ function createEntries() { function createEntry(config) { const isGlobalBuild = config.format === 'iife' const isBundlerBuild = config.format !== 'iife' && !config.browser + const isBundlerESMBuild = config.format === 'es' && !config.browser const c = { external: ['vue'], @@ -61,7 +62,9 @@ function createEntry(config) { __DEV__: isBundlerBuild ? `(process.env.NODE_ENV !== 'production')` : config.env !== 'production', - __VUE_PROD_DEVTOOLS__: isBundlerBuild ? '__VUE_PROD_DEVTOOLS__' : 'false' + __VUE_PROD_DEVTOOLS__: isBundlerESMBuild + ? '__VUE_PROD_DEVTOOLS__' + : 'false' })) if (config.transpile !== false) { diff --git a/src/plugins/devtool.js b/src/plugins/devtool.js index f68bf6823..7b9b26a54 100644 --- a/src/plugins/devtool.js +++ b/src/plugins/devtool.js @@ -59,7 +59,7 @@ export function addDevtools (app, store) { makeLocalGetters(store, modulePath) payload.state = formatStoreForInspectorState( getStoreModule(store._modules, modulePath), - store._makeLocalGettersCache, + modulePath === 'root' ? store.getters : store._makeLocalGettersCache, modulePath ) } @@ -228,16 +228,45 @@ function formatStoreForInspectorState (module, getters, path) { } if (gettersKeys.length) { - storeState.getters = gettersKeys.map((key) => ({ + const tree = transformPathsToObjectTree(getters) + storeState.getters = Object.keys(tree).map((key) => ({ key: key.endsWith('/') ? extractNameFromPath(key) : key, editable: false, - value: getters[key] + value: canThrow(() => tree[key]) })) } return storeState } +function transformPathsToObjectTree (getters) { + const result = {} + Object.keys(getters).forEach(key => { + const path = key.split('/') + if (path.length > 1) { + let target = result + const leafKey = path.pop() + path.forEach((p) => { + if (!target[p]) { + target[p] = { + _custom: { + value: {}, + display: p, + tooltip: 'Module', + abstract: true + } + } + } + target = target[p]._custom.value + }) + target[leafKey] = canThrow(() => getters[key]) + } else { + result[key] = canThrow(() => getters[key]) + } + }) + return result +} + function getStoreModule (moduleMap, path) { const names = path.split('/').filter((n) => n) return names.reduce( @@ -251,3 +280,11 @@ function getStoreModule (moduleMap, path) { path === 'root' ? moduleMap : moduleMap.root._children ) } + +function canThrow (cb) { + try { + return cb() + } catch (e) { + return e + } +}