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 statedata()ref() / reactive()
actionsmethods普通の関数
shared store の利用data()store を差し込むscript setup で直接 import して使う
再利用単位mixins やコンポーネント外ユーティリティcomposable + reactive primitives

Key Concepts

用語説明
stateアプリケーションの事実の源泉
viewstate から導かれる宣言的なUI
actionsstate を変更する操作
one-way data flowstate view、action state という一方向の流れ
shared state複数コンポーネントで共通利用する状態
single source of truth状態を一箇所に集約して矛盾を防ぐ考え方
prop drilling深い階層へ props を受け渡し続けることで複雑化する問題
singleton storeモジュールスコープで1つだけ生成される共有store
SSRサーバーで初期HTMLを生成する構成
cross-request state pollutionSSRで別リクエスト間に状態が混入する問題
PiniaVue公式推奨の state management ライブラリ

実務での判断軸

  • shared state が少なく小規模なら composable + ref / reactive で十分な場合がある
  • 複数画面、複数コンポーネント、長期運用なら state の集中管理が重要になる
  • SSR、DevTools、TypeScript、チーム開発を重視するなら Pinia が本線になる
  • state management の本質は「どこに置くか」だけでなく、「誰がどう変更し、その変更をどう追跡するか」の設計にある