採点されません。匿名です。 間違えることは学習の一部です。読むだけでもかまいません。

Vue — Pinia 画面間共有

テクノロジ系 / L05 state-management — 状態管理

Vue — Pinia 画面間共有

state-management / ch03

このチャプターの到達ゴール

翌日
画面間で同じデータを使う必要が出たとき、 props 伝達ではなく store を選べる
storeToRefs を忘れたときに何が起きるかを 1 文で言える
「3 つ目の画面を生やすコスト」 を 説明できる

> 答えを暗記するのではなく、 痛み → 解 の順で身につける。

なぜ store か (= 動機の言語化)

1 画面のタスクアプリに「タスク追加」 画面を増やそうとすると こうなる:

TaskAddView (追加した) → 親 App.vue に emit → TaskListView に props で渡す

これが props バケツリレー。 画面が増えるほど App.vue が肥大化する。

> 解決: tasks を store という外部の置き場所に出す。 全画面から 直接 store を見にいける

> これが Pinia が解いてくれる痛み。

最小 store を 1 ファイルで書く

src/stores/tasks.js:

import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useTasksStore = defineStore('tasks', () => {
  // state (= 持っているデータ)
  const tasks = ref([])

  // action (= state を変える関数)
  async function load() {
    const res = await fetch('http://localhost:8000/tasks')
    tasks.value = await res.json()
  }

  return { tasks, load }
})

用語の整理

名前役割このチャプターでの例
state持っているデータtasks (= ref)
getterstate から派生する値(本 chapter では出さない / forms 章で computed として登場)
actionstate を変える関数load(), add()

> computed が getter / ref が state / 関数が action という対応で覚える。

TaskListView を store ベースに書き換える

<script setup>
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useTasksStore } from '@/stores/tasks'

const store = useTasksStore()
const { tasks } = storeToRefs(store)     // ⚠️ storeToRefs を忘れない

onMounted(() => store.load())
</script>

<template>
  <h1>タスク一覧</h1>
  <ul>
    <li v-for="t in tasks" :key="t.id">{{ t.name }}</li>
  </ul>
</template>

storeToRefs の罠 (= 必ず踏む)

// ❌ NG: リアクティブが切れる (= store が変わっても画面が更新されない)
const { tasks } = store

// ✅ OK: ref のまま取り出す
const { tasks } = storeToRefs(store)

// ✅ OK: action は分割代入してよい (= ref ではないため)
const { load, add } = store

> ルール: ref/computed (= state/getter) は storeToRefs、 関数 (= action) は普通の分割代入

なぜ storeToRefs を忘れると壊れるのか

const { tasks } = storeJavaScript の分割代入 で 「現時点の tasks の値」 を取り出す動き。

取り出した瞬間に store との つながりが切れる ので、 後で store の tasks が変わっても画面に伝わらない。

storeToRefs(store) は 「ref のまま (= リアクティブの管を保ったまま) 取り出す」 専用の関数。

管がつながっていれば、 store 側で tasks.value.push(...) した瞬間に画面が再描画される。

> 「呪文として暗記する」 ではなく、 分割代入が値を切り離す という JavaScript の素の仕様から来ている。

action を 1 つ追加 (= add)

export const useTasksStore = defineStore('tasks', () => {
  const tasks = ref([])

  async function load() { /* 既出 */ }

  async function add(name) {
    const res = await fetch('http://localhost:8000/tasks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name }),
    })
    const created = await res.json()
    tasks.value.push(created)
  }

  return { tasks, load, add }
})

TaskAddView を作る (= 2 画面目)

<script setup>
import { ref } from 'vue'
import { useTasksStore } from '@/stores/tasks'
import { useRouter } from 'vue-router'

const store = useTasksStore()
const router = useRouter()
const name = ref('')

async function onSubmit() {
  await store.add(name.value)
  router.push('/')             // 一覧へ戻る
}
</script>

<template>
  <h1>タスク追加</h1>
  <input v-model="name" placeholder="タスク名" />
  <button @click="onSubmit">追加</button>
</template>

動作確認 (= 状態共有の体感)

  1. 「タスク追加」 で入力 → 戻ると 一覧に増えている
  2. DevTools の Pinia タブ (= 樽マーク) で tasks 配列が伸びている
  3. TaskAddView と TaskListView の間に 直接の親子関係がない のに 状態が共有されている

> ここが Pinia の効能: ページが変わっても store は消えない (= router 遷移と独立)。

「props バケツリレーを撤廃した証拠」

観点Pinia なしPinia あり
App.vue に tasks を置く必要ありなし
TaskAddView から TaskListView へ emit/props必要不要
3 つ目の画面を生やすコスト親に props 追加で配線必要store を読むだけ

> 「3 つ目の画面を生やすコスト」 が劇的に下がっているのが体感できる。

チャプター完了サイン

  • [ ] 「タスク追加」 で入力 → 戻ると 一覧に増えている
  • [ ] Pinia DevTools で tasks 配列が伸びている
  • [ ] 「なぜ store が要るのか」 を 自分の言葉で説明できる (= props バケツリレーの痛みへの解)
  • [ ] 「なぜ storeToRefs を忘れると壊れるのか」 を 1 文で言える

次に学ぶ領域 (= 名前だけ)

領域名前
Pinia 別形式Options Store (= defineStore({state, getters, actions}))
state 操作 API$patch / $reset / $subscribe
永続化pinia-plugin-persistedstate
ストア間連携store A 内で useStoreB() を呼ぶ / circular dep 回避
テストsetActivePinia + Vitest で store 単体テスト
バリデーションforms/ch01-reactive-validation
非同期 UXapi/ch01-async-ux-3states

> 全部覚える必要はない。 「今日の store のテストが書ける」 「永続化できる」 という地図だけ持っておく。

考えてみよう

既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。

ch03 — 演習

本編 manuscript と同じ題材 (タスク管理ミニアプリ + Mock API) で動かす想定。

API は port 8000 で動いている前提:

  • GET /tasks[{ id, name }, ...]
  • POST /tasks{ id, name }

Lv1: store を最小で書く

stores/tasks.js を以下の仕様で書け。

// stores/tasks.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useTasksStore = defineStore('tasks', () => {
  // TODO 1: tasks = ref([]) (= state)
  // TODO 2: async function load() で GET /tasks を tasks に入れる
  // TODO 3: tasks, load を return する
})

検証

TaskListView.vuestoreToRefs で取り出した tasks<ul><li v-for> で表示し、 起動直後にデータが見えれば OK。

Lv2: storeToRefs の罠を踏む / 解消する

下記 2 つのコードを順番に試して、 何が起きるかを観察せよ。

<!-- 試行1: NG パターン -->
<script setup>
import { useTasksStore } from '@/stores/tasks'
const store = useTasksStore()
const { tasks } = store          // ← 分割代入
// 起動後に store.tasks.value.push(...) で追加してみる
</script>

→ 画面が更新されない (= reactivity が切れている)。

<!-- 試行2: OK パターン -->
<script setup>
import { storeToRefs } from 'pinia'
import { useTasksStore } from '@/stores/tasks'
const store = useTasksStore()
const { tasks } = storeToRefs(store)   // ← storeToRefs 経由
</script>

→ 画面が更新される。

提出物

なぜ storeToRefs を使うと更新されるのか」 を 1 文で書け。

Lv3: 2 画面で同じ store を読む

TaskListView.vue (= 一覧) と TaskAddView.vue (= 追加フォーム) の 両方

useTasksStore を呼び、 「追加した瞬間に一覧が更新される」 ことを確認せよ。

検証

操作期待
/add で名前入力 → 追加ボタン押下router.push('/') で一覧へ遷移
一覧に追加した名前が見えるOK
ブラウザ DevTools の Pinia タブtasks 配列が伸びている
TaskAddView と TaskListView の間に親子関係なし(template を見れば確認できる)

提出物

props バケツリレーが消えた」 ことを 自分の言葉で 1 文で書け。

ヒント

  • storeToRefsimport し忘れる のが 1 番多い。 import { storeToRefs } from 'pinia'
  • store.load()onMounted で呼ばないと初期データが空のまま
  • store.add(name) の引数を trim しないと 空白だけのタスクが追加される (= 別 chapter forms/ で対処)