state-management について
state-management は、Vue 開発者向けのアプリケーション状態管理ガイドです。props やカスタムイベントから共有ストア、Pinia までを扱い、アプリの複雑さに応じた選択肢とトレードオフを説明します。
主な機能
- 01props とカスタムイベントによる通信の解説
- 02reactive() を使った簡易ストアの紹介
- 03Pinia によるアプリ状態の管理
- 04各方式の適用場面とトレードオフの比較
活用シーン
- 01親子コンポーネント間でのデータとイベントの受け渡し
- 02兄弟または深い階層のコンポーネント間での状態共有
- 03大規模なアプリでの状態とアクションの整理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill state-managementインストール後、Claude Code で/state-managementを入力して読み込まれたか確認します。
詳細
状態管理
目次
Vue コンポーネントは Vue アプリの構成要素です。コンポーネント内でマークアップ(HTML)、ロジック(JS)、スタイル(CSS)をまとめて扱えます。
利用する場面
- 兄弟コンポーネント間、または深くネストされたコンポーネント間でリアクティブなデータを共有する必要がある場合に使用します
- 単純な親子間の props の受け渡しだけでは不十分な、アプリケーション全体の状態管理に役立ちます
手順
- 親から子へのデータの受け渡しには props を使用し、子から親への通信にはカスタムイベントを使用します
- 小規模なアプリケーションでは、
reactive()を使ってシンプルなストアを作成します - 開発者ツール、プラグイン、TypeScript のサポートが必要な大規模アプリでは、Pinia(公式の Vue 状態管理ライブラリ)を使用します
- アプリの複雑さに応じて状態管理の方法を選び、小規模なアプリを過剰に設計しないようにします
詳細
データプロパティの数値を並べて表示する、単一ファイルコンポーネントの例を示します。
<template>
<div>
<h2>The numbers are {{ numbers }}!</h2>
</div>
</template>
<script setup>
import { ref } from "vue";
const numbers = ref([1, 2, 3]);
</script>
ref() 関数は、コンポーネントを リアクティブ にします。テンプレートで使用しているリアクティブなプロパティの値が変わると、コンポーネントの表示が再レンダリングされ、変更が反映されます。
numbers が別のコンポーネントからアクセスする必要のあるデータ値だとしたら、どうでしょうか?複数のコンポーネント間で numbers を共有する場合、numbers はコンポーネントレベルのデータ だけでなく、アプリケーションレベルのデータにもなります。ここで、アプリケーションレベルのデータを管理する 状態管理 について説明します。
Props
Vue では、props を使って親から子へデータを渡せます。props の使用方法はとても簡単です。基本的には、子コンポーネントを表示する箇所で、prop 属性に値をバインドするだけです。
ParentComponent:
<template>
<div>
<ChildComponent :numbers="numbers" />
</div>
</template>
<script setup>
import { ref } from "vue";
import ChildComponent from "./ChildComponent";
const numbers = ref([1, 2, 3]);
</script>
ChildComponent:
<template>
<div>
<h2>{{ numbers }}</h2>
</div>
</template>
<script setup>
const { numbers } = defineProps(["numbers"]);
</script>
コンポーネントイベント
反対方向に情報を伝える方法が必要な場合はどうでしょうか?props は親から子へ一方向にしかデータを渡せないため、props は使用できません。子コンポーネントから親コンポーネントに何かを知らせるには、カスタムイベントを使用できます。
Vue のカスタムイベントはネイティブの CustomEvents としてディスパッチされ、コンポーネント間の通信に使用されます。
ChildComponent:
<template>
<div>
<h2>{{ numbers }}</h2>
<input v-model="number" type="number" />
<button @click="$emit('number-added', Number(number))">
Add new number
</button>
</div>
</template>
<script setup>
const { numbers } = defineProps(["numbers"]);
</script>
ParentComponent:
<template>
<div>
<ChildComponent :numbers="numbers" @number-added="(n) => numbers.push(n)" />
</div>
</template>
<script setup>
import { ref } from "vue";
import ChildComponent from "./ChildComponent";
const numbers = ref([1, 2, 3]);
</script>
シンプルな状態管理
props を使ってデータを下方向に渡し、カスタムイベントを使って上方向にメッセージを送ることができます。では、異なる 2 つの兄弟コンポーネントの間でデータを渡したり、通信を可能にしたりするにはどうすればよいでしょうか?
アプリケーションレベルの状態を管理するシンプルな方法は、コンポーネント間でデータストアを共有するストアパターンを作成することです。ストアでは、アプリケーションの状態と、状態の変更を担うメソッドを管理できます。
import { reactive } from "vue";
export const store = reactive({
numbers: [1, 2, 3],
addNumber(newNumber) {
this.numbers.push(newNumber);
},
});
ストアには numbers 配列と、ペイロードを受け取ってストアの numbers の値を直接更新する addNumber メソッドが含まれています。
Vue 3.x では、reactive() 関数をインポートして使うことで、JavaScript オブジェクトからリアクティブな状態を宣言できます。このリアクティブな状態が addNumber() メソッドによって変更されると、このリアクティブな状態を使っているコンポーネントはすべて自動的に更新されます!
NumberDisplay:
<template>
<div>
<h2>{{ store.numbers }}</h2>
</div>
</template>
<script setup>
import { store } from "../store.js";
</script>
NumberSubmit:
<template>
<div>
<input v-model="numberInput" type="number" />
<button @click="store.addNumber(numberInput)">Add new number</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import { store } from "../store.js";
const numberInput = ref(0);
</script>
ここでコンポーネント同士が連携すると言うとき、「連携」という言葉は厳密な意味では使っていません。コンポーネントが互いに何かをするのではなく、ストアを_通じて_互いの変更を引き起こします。
ストアと直接やり取りする要素を詳しく見ていくと、次のようなパターンがわかります。
NumberSubmit内のメソッドは、ストアのメソッドに直接作用する役割を担うため、ストアアクションと呼べます。- ストアのメソッドにも、ストアの状態を直接変更するという特定の役割があります。そのため、ストアミューテーションと呼びます。
NumberDisplayは、ストアやNumberSubmitにどのような種類のメソッドがあるかを気にせず、ストアから情報を取得することだけに関心があります。そのため、NumberDisplayはある種のストアゲッターと呼べます。
アクションがミューテーションを実行します。ミューテーションが状態を変更し、それによってビュー/コンポーネントに影響します。ビュー/コンポーネントはゲッターを使ってストアのデータを取得します。アプリケーションレベルの状態をより構造化された方法で扱うための考え方に近づいてきました。
Pinia
Pinia は、アプリケーションレベルの状態をより構造的かつスケーラブルに扱うための、Vue.js 向け状態管理パターン兼ライブラリです。
Pinia は Vuex などの状態管理ソリューションに代わるもので、現在は Vue の公式状態管理ライブラリです。状態、アクション、ゲッターをカプセル化するストアを簡単かつ効率的に作成、管理できます。
Pinia では、defineStore() 関数を使ってストアを定義できます。ここでは Composition API の構文を使って useNumbersStore() 関数を定義し、numbers ストアを作成します。
import { ref } from "vue";
import { defineStore } from "pinia";
export const useNumbersStore = defineStore("numbers", () => {
const numbers = ref([1, 2, 3]);
function addNumber(newNumber) {
numbers.value.push(newNumber);
}
return { numbers, addNumber };
});
続いて、Pinia のインスタンスを作成し、Vue アプリにインストールします。
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount("#app");
NumberDisplay コンポーネントでは:
<template>
<div>
<h2>{{ store.numbers }}</h2>
</div>
</template>
<script setup>
import { useNumbersStore } from "../store";
const store = useNumbersStore();
</script>
NumberSubmit コンポーネントでは:
<template>
<div>
<input v-model="numberInput" type="number" />
<button @click="store.addNumber(numberInput)">Add new number</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useNumbersStore } from "../store";
const store = useNumbersStore();
const numberInput = ref(0);
</script>
このようにシンプルな実装であれば、Pinia のストアは必ずしも必要ではなく、reactive() 関数で作成したストアを使う場合と非常によく似た動作になります。ただし、Pinia には、プラグインによる機能の拡張、開発者ツールのサポート、より適切な TypeScript とサーバーサイドレンダリングのサポートなど、複雑なユースケースに対応する追加機能があります。
正しい方法とは?
アプリケーションレベルの状態を管理する各方法には、それぞれ長所と短所があります。
シンプルなストア
- 長所:比較的簡単に導入できます。
- 短所:状態と、想定される状態の変更が明示的に定義されていません。
Pinia
- 長所:開発者ツール、プラグイン、TypeScript、サーバーサイドレンダリングをサポートします。
- 短所:追加のボイラープレートが必要です。
最終的には、アプリケーションに何が必要か、どの方法が最適かを理解したうえで判断する必要があります。