State Management - 共有状態の設計とPiniaが必要になる理由
ロードマップ: Vue.js学習ロードマップ
What(何についてか)
State management は、アプリケーションの状態(state)をどこに置き、どのように変更し、どの画面がそれを参照するかを設計する考え方である。Vueでは各コンポーネントが自身の local state を持てるが、複数コンポーネントで shared state が必要になると、管理方針が重要になる。
Vue公式は state を次の3要素で整理する。
- state: アプリケーションの事実の源泉
- view: state から導かれる宣言的な表示
- actions: state を変化させる操作
Why(なぜ必要か)
単一コンポーネント内では local state だけで十分なことが多いが、次の状況で問題が発生する。
- 複数の view が同じ state に依存する
- 複数の view から同じ state を変更したい
この問題に対して、shared state を親コンポーネントへ持ち上げて props で配る方法はあるが、階層が深くなると prop drilling が発生する。また、emits による更新リレーや template ref による直接参照は脆く、保守性を下げやすい。
そのため、複数コンポーネントで使う shared state はコンポーネントツリーの外に出し、single source of truth として管理するほうが合理的である。
How(どう動くか)
One-way data flow
単一コンポーネントでは、Vueの基本構造は次のように整理できる。
flowchart TD A["state"] --> B["view"] C["user action"] --> D["actions"] D --> A
この一方向の流れが、shared state を扱う設計でも基礎になる。
reactive() による簡易 shared store
Vueでは、モジュールスコープで reactive() を使って shared state を作成し、複数コンポーネントから import して共有できる。
// store.js
import { reactive } from 'vue'
export const store = reactive({
count: 0
})<script setup>
import { store } from './store.js'
</script>
<template>From A: {{ store.count }}</template><script setup>
import { store } from './store.js'
</script>
<template>From B: {{ store.count }}</template>この構成では、全コンポーネントが同じ store オブジェクトを共有し、どこかで store.count が変更されると、依存している view が自動で更新される。
flowchart TD S["shared reactive store"] --> A["Component A"] S --> B["Component B"] A --> VA["View A"] B --> VB["View B"]
変更ロジックの集中
shared state を誰でも直接変更できる設計は、長期的には保守しにくい。そこで、state と同じ場所に更新メソッドを定義し、変更ルールを集約する。
import { reactive } from 'vue'
export const store = reactive({
count: 0,
increment() {
this.count++
}
})<template>
<button @click="store.increment()">
From B: {{ store.count }}
</button>
</template>これにより、状態の置き場だけでなく変更責務も store 側へ寄せられる。
Composable との接続
shared state をコンポーネント外へ出すという発想は composable と地続きである。Vue公式でも、reactive() だけでなく ref() や computed()、または composable から返す形で global state を共有できると説明されている。
import { ref } from 'vue'
const globalCount = ref(1)
export function useCount() {
const localCount = ref(1)
return {
globalCount,
localCount
}
}ただし、このページの主題は composable の詳細ではなく、shared state の設計思想そのものにある。
SSR Considerations
reactive() をモジュールスコープで singleton として作る方法は、SPAでは単純で有効だが、SSRでは注意が必要である。サーバー上では複数リクエストが同一プロセスで処理されるため、singleton store にあるリクエストの状態が別のリクエストへ混入する危険がある。
flowchart TD A["Request A"] --> S["singleton store"] B["Request B"] --> S S --> R["cross-request state pollution"]
そのため、SSRではリクエストごとに独立した store 生成や、SSR対応済みライブラリの採用が重要になる。
Pinia
大規模アプリケーションでは、shared state を置けるだけでは不十分である。Vue公式は、次の理由から Pinia を推奨している。
- チーム開発向けの強い規約
- Vue DevTools 連携
- timeline / inspection / time-travel debugging
- Hot Module Replacement
- SSR support
- TypeScript の強い型推論
Pinia は Vue core team によりメンテされており、Vuex の後継的ポジションとして新規Vueアプリの標準的選択肢である。Vuex は現在 maintenance mode に入っている。
Options API → Composition API 差分(補足)
| 項目 | Options API(旧) | Composition API(新) |
|---|---|---|
| local state | data() | ref() / reactive() |
| actions | methods | 普通の関数 |
| shared store の利用 | data() に store を差し込む | script setup で直接 import して使う |
| 再利用単位 | mixins やコンポーネント外ユーティリティ | composable + reactive primitives |
Key Concepts
| 用語 | 説明 |
|---|---|
| state | アプリケーションの事実の源泉 |
| view | state から導かれる宣言的なUI |
| actions | state を変更する操作 |
| one-way data flow | state → view、action → state という一方向の流れ |
| shared state | 複数コンポーネントで共通利用する状態 |
| single source of truth | 状態を一箇所に集約して矛盾を防ぐ考え方 |
| prop drilling | 深い階層へ props を受け渡し続けることで複雑化する問題 |
| singleton store | モジュールスコープで1つだけ生成される共有store |
| SSR | サーバーで初期HTMLを生成する構成 |
| cross-request state pollution | SSRで別リクエスト間に状態が混入する問題 |
| Pinia | Vue公式推奨の state management ライブラリ |
実務での判断軸
- shared state が少なく小規模なら composable +
ref/reactiveで十分な場合がある - 複数画面、複数コンポーネント、長期運用なら state の集中管理が重要になる
- SSR、DevTools、TypeScript、チーム開発を重視するなら Pinia が本線になる
- state management の本質は「どこに置くか」だけでなく、「誰がどう変更し、その変更をどう追跡するか」の設計にある