みなさんこんにちは。私の名前はEduardoです。Vue.jsコアチームの一員で、Vue Routerの作者でもあります。また、PiniaというVuexの代わりに使えるComposition API向けのちょっとした状態管理ライブラリも作っています。
ただ、今日は、特に状態管理については取り上げるつもりはありません。Piniaのこれからについてお話しするつもりです。 ただし、最初に少し状態管理についての話をしなければその先に進められませんので少しお話ししようと思います。
いくつかの関数の状態管理で変更するのは単なるオブジェクトでしょうか?それが正しいという人もいますし、そして彼らは完全に間違っているわけではありません…
そのオブジェクトをreactive
で囲むと、Vueとシームレスに連動する有効な状態管理が可能になります。そんなに複雑じゃありませんよね?実のところ、状態管理は複雑である必要はなく、うまくやれば、さらに大きなアプリケーションでもシンプルに保つことが可能です。世の中には、アプリケーションが中規模以上になると状態管理が必要になってくるという神話がありますが、これは事実とはまったく違います。
状態管理はアプリケーションやコードベースの大きさによるものではありません。あなたが状態をアプリケーション全体でどのように使用するかによります。ページよりも長く状態が保持されていますか?たくさんの場所でユーザー情報が必要ですか?複雑にネストされたコンポーネントツリーの複数の場所からショッピングカートの状態が必要になりますか? しかし今日、私が皆さんにぜひ考えていただきたい問いかけは、何があなたに状態管理ソリューションを前進させ、また何がそれを複雑にさせているのか、ということです。
オブジェクトを使ったソリューションはほとんどのケースでうまくいくので、ストアを使ってもう少し複雑なことをするのは気が進まないかもしれません。 つまり、ここから状態管理を進化させるには当然ながらストアを使用することになりますが、実際のところストアが絶対に必要というわけではありません。 ここで重要なのは、素の状態管理ソリューションとPiniaのようなストアの違いはそれほど大きくないということです。
言い換えれば、本当にストアが必要なのか、それともリアクティブなオブジェクトを使うだけの状態管理ソリューションを使い続けることができるのかということです。
実際のところ、オブジェクトにプロパティを追加し続けると一元化されたオブジェクトは拡張性に欠けるために、ストアを使用する理由はいくつかあります。一つ目の理由は、サーバーサイドレンダリングです。アプリケーションでSSRを行う場合、状態管理を自前で行うことはまったく無駄な努力です。ストアを使えばスケーリングの問題なく通知されたすべての異なる状態を一括して保存する場所についても提供してくれます。またストアは、devtoolsやHMR可能にし、他の開発者によって作られた既存のプラグインを活用することで、より優れた開発者体験を与えてくれます。さらにストアに依存するコンポーネントのユニットテストを行うためのモッキングも簡単に行うことができ、全体として堅牢かつ実用的なソリューションとなっています。
では、単純な状態管理ソリューションからPiniaのようなストアに移行したい場合はどうでしょうか。まず、Piniaとは何でしょうか? 全ての人が出くわす簡単な例を見てみましょう。これは新しいプロジェクトのmain.jsまたはmain.TSファイルです。
Piniaは、インスタンスを作成してアプリケーションに渡すだけで使い始めることができます。 そのPiniaオブジェクトは、状態管理がどのように機能するかにおいて実際に中心的な役割を果たしています。これは、これから行うさまざまな状態をすべて一元化し、DevtoolsとSSRを有効にするためです。
そのPiniaは、実際には、空のオブジェクト、または最初は空のオブジェクトの単なる参照である状態プロパティを保持しています。
以前に定義したストアを初めて使用し始めるとすぐに、定義した状態が入力されます。たとえば、初回に useAuthStore
を呼び出すと、初期状態のオブジェクトが pinia.state.value.auth
に追加されます。その関数を再度呼び出すと、その状態が再利用され、アプリケーションがブラウザで開いている限り、その状態が維持されます。また、呼び出す他の useStore
関数についても同じです。
ストアの状態をすべてひとつのオブジェクトに集めるというシンプルなアイデアが、Piniaの拡張を容易にしています。devtools、SSR、プラグインなどの統合を容易にしているのです。そしてそれは、Piniaが誕生した当初から存在しています。
そしてそれはずいぶん前のことです。最初のPiniaのバージョンはVue 3の前に登場したので、Vue 2 + Composition APIにしか対応していませんでした。当時は、VuexとVue Devtools 5の通信チャネルをハックして、既存のdevtoolsの恩恵を受け、実際のストアでの体験を提供していました。
初期のバージョン1では、ほんの数ヶ月前に登場したEffectScope APIがなかったため、Piniaは内部のdevtools関数を呼び出し、useStoreを呼び出すたびに重複したゲッターとアクションを使用していました。また、ストアの機能を拡張するためのプラグインインターフェースもなく、ストアを定義する方法は、オプションAPIを使用することのみでした。要するに、明らかに実験的なものであり、本番環境には対応していませんでした。重要なのは、APIの形とそれを使った開発者の体験でした。TypeScriptをサポートし、すべてをSSRで動作させることに大きな重点が置かれました。
この間に物事は大きく進展しました。Vue 2とVue 3の両方をサポートしているだけでなく、ストアを定義するための追加の構文があり、コンポーネントに近い形になっています。それは、すでにCompoistion APIを使っているなら、とてもとても馴染み深いものです。また、ストアに依存するコンポーネントをテストする際に、作業を容易にするテストモジュールも用意されています。Nuxt 2、Bridge、Nuxt 3をサポートするNuxtモジュール。そして最後に、完全に型付けできる非常に完成度の高いプラグインインターフェイスがあります。
さらに、PiniaのAPIの核心部分は、Vuex 5のAPIに影響を与えており、RFCで確認することができます。つまり、PiniaとVuexの両方で、Storeの定義とその相互作用はほぼ同じです。これにより、必要に応じて、一方のバージョンから他方のバージョンへの切り替えが容易になります。
実は、Vuex 4とPiniaの最大の変更点は、Vuex 4と Vuex 5と同じです。Vuex 4では、createStoreでストアを作成し、そこにミューテーション(mutation)を追加して状態を変更させることができるようになっています。
Vuex 5とPiniaでは、ストアの状態を直接変更させることができます。冗長で不必要なミューテーションはもう必要ありません。しかし、Piniaで状態を変更させる方法はそれだけではありません。
Piniaでは、$patch
メソッドを使って、変更したい状態の部分的なバージョンを渡すこともできます。あるいは、状態を直接変更させる関数を渡すこともできます。
これは、Set、Map、Arrayどのコレクションを扱うときに便利です。また、状態の変更をグループ化することで、ストアへのサブスクリプションが一度だけトリガーされるようになります。配列で複数のプロパティを監視するのと似ています。
これは、ストアの変更やストアのアクションを購読したり、作成されたすべてのストアに必要なプロパティを追加したりできるプラグインでは特に便利です。 pinia のプラグインは、現在実行中のアプリケーション、Pinia オブジェクト、プラグインが適用されるストア、ストアの定義に使用されたオプションを含むコンテキストオブジェクトを受け取る関数です。
Piniaプラグインでは、主に3つのことを行います。状態の変化を購読する。これは、Vuexにもあることです。変更の種類、ストアのID、状態の変更方法に応じたペイロード、問題のストアの現在の状態にアクセスできます。 アクションが実行される前や後、失敗したとき、あるいはキャンセルしたときに、特別な処理を引き起こすことができます。 そして最後に、ストアに任意のプロパティを追加することができます。例えば、ストア内のプロパティを返すだけで、すべてのストアにルーターへの参照を追加することができます。そして、アクションやゲッター、またはstore.routerから直接アクセスすることができます。
これは、アクションで発生したエラーをSentryのような外部サービスにプラグインのシンプルな例です。 これら4行のコードは、本番環境でバグを検出するために、あなたがストアからエラーをレポートることができます。
そして、これらのことをすべて、Piniaは簡単に拡張できるようになっています。実際、Pinia向けのdevtoolsインターフェイスはプラグインで処理されています。
それでは、実際にデモを見てみましょう。
シンプルなページをViteのDevサーバーで動かします。これは単一のプロパティを持ったとてもシンプルなカウンタストアを使っていて、そして下の方にはHMRセットアップがあります。つまり、プロパティを追加したり削除したりしても、アプリケーションはリロードせずに更新され、アプリケーションの状態はそのまま維持されます。
devtoolsでは、ストアを使用してコンポーネントを検査(inspect)し、必要に応じてその状態を変更することができます。また、タイムラインでは、ストアに起こっているさまざまな変化を見ることができます。 これからカウンターがゼロになるまでデクリメントするアクションを追加します。アクションを最後まで数秒持続させるために、各デクリメントの間に数ミリ秒の待ち時間を設けています。
これで、必要なコードはすべて揃いました。あとはページにボタンを追加して、このメソッドを呼び出します。Volarのおかげで自動補完機能もついていて、とてもいい感じです。
ここで私達は何を見ようとしているのでしょうか?カウンターの減少がどのように周期的に起こるか、そして、タイムライン上で個別に確認できることを、見たいです。タイムラインをクリアにして、ボタンをクリックしましょう。
ご覧のとおり、アクションが状態を変更するたびにドットが表示されます。その上、すべてこれらの変化は、同じアクションの中で起こっているので、それらはすべて1つの長い黄色いフレームにまとめて表示されます。実際、最初からあったインクリメントミューテーションを呼び出すと、新しいドットが現れますが、それらは ミューテーションはその外で発生しているので、既存の実行中のアクションとは混ざることはありません。
複数のアクションを並行実行しても、それらミューテーションは混ざってしまうことはありません。そして、本当に素晴らしいのは、イベントをグループ別に見るのを選択することで、全てのアクションを個別に検査できることです。例えば、最も時間の掛かったアクションは、x ミリ秒そして、x ミューテーションでした。しかし、より短いアクションを調べれば、異なるイベント数と期間を見ることができます。そして、1つ1つのドットで、変化した正確な状態を、その前の値と現在の値で調べることができます。
またここから、ストアインスペクターにアクセスして、そこから何かの値に変更することできます。ストアが decrease 関数を実行している間に何かの値に変更しても、シームレスに動作します。ストアを直接変更してもタイムラインにノイズが残らないので、アプリケーションからの変更を正しく検査し、デバッグからの変更を無視することができます。また、アクションの中で状態の複数の部分を直接変更することができます。例えば、カウンターが減少した回数をカウントし、それを試してタイムラインをチェックすると、より多くのミューテーションが発生している事がわかります。しかし、$patch()
関数をを使って、ミューテーションをグループ化することができます。そうすることで、タイムラインにミューテーションがグループ化されていることが分かります。
意外と知られていないことですが、実はステートには任意のコンポーザブル(Composition APIで実装されたコード)を渡すことができます。あらゆる ref
や computed
が動作します。つまり、@vueuse/core
の useLocalStorage()
メソッドを n
プロパティに渡すだけで、動作するのです。このシナリオでは、ステート内の既存のプロパティの性質を変更したので、まだページをリロードする必要があります。そして、カウンターを変更してページをリロードすると、その値がローカルストレージから復元されるのがわかります。また、タイムラインやストアインスペクタなど、他のすべての機能も動作します。
これがストアのオプション構文です。非常にわかりやすく、開発者なら誰でも、初めてストアを見た人でも理解できるはずです。しかし、VueのオプションAPIと同じように、この構文には制限があります。そこで、setup()
構文とほぼ同じで、使用できる代替構文があります。
NASAのAPIを使って今日の写真を取得する長い例がありますが、ストアのコード全体に目を通す時間はありません。しかし、プロパティを持つオブジェクトの代わりに、1つの関数があることがお分かりいただけると思います。状態を作るには ref
や reactive
を使い、ゲッターを作るには computed
を呼び出し、アクションを作るには関数を作ります。また、コンポーザブルを使うこともできます。例えば、キャッシュされたAPIコールを持っているので、いくつかの写真をチェックしたら、ページ間の時間を全く待たずに簡単に戻ることができます。また、面白いことに、ストアインスペクタにアクセスすると、カウンターに並んで新しいストアが表示されているのがわかります。これは、Pinia(そしてVuex 5)では、すべてのストアが動的に登録されるため、何もしなくてもパフォーマンスとバンドルサイズが向上するためです。
最後になりましたが、オープンソースにフルタイムで取り組めるようにしてくれたスポンサーの皆様に感謝したいと思います。もしあなたがVue.jsを使っている企業であれば、いくつかの興味深いオプションがあります。私のスポンサーページをご覧ください。 まだPiniaを使ったことがない方は、ぜひ試してみてください。