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

Vue — Pinia

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

Vue — Pinia

Pinia とは

Vue 3 公式推奨の 状態管理ライブラリ:

  • Vuex の後継
  • TypeScript 親和性高
  • ボイラープレート少ない
  • DevTools サポート
  • Composition API ネイティブ
$ pnpm add pinia

setup

main.ts:

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

createApp(App).use(createPinia()).mount("#app");

store 定義 (Setup Store スタイル・推奨)

stores/counter.ts:

import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useCounterStore = defineStore("counter", () => {
  // state
  const count = ref(0);

  // getter
  const doubled = computed(() => count.value * 2);

  // action
  function increment() {
    count.value++;
  }
  async function fetchInitial() {
    const res = await fetch("/api/count");
    count.value = (await res.json()).count;
  }

  return { count, doubled, increment, fetchInitial };
});

<script setup> と同じ感覚で書ける。

使う

<script setup>
import { useCounterStore } from "@/stores/counter.js";
import { storeToRefs } from "pinia";

const counter = useCounterStore();
const { count, doubled } = storeToRefs(counter);   // reactivity を保つ
const { increment } = counter;                       // actions は分割可
</script>

<template>
  <p>{{ count }} ({{ doubled }})</p>
  <button @click="increment">+</button>
</template>

storeToRefs忘れない

const { count } = counter だとリアクティブが切れる。

Options Store スタイル

export const useCounterStore = defineStore("counter", {
  state: () => ({ count: 0 }),
  getters: {
    doubled: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++;
    },
  },
});

Vuex に近い書き方。

新規は Setup Store が公式推奨。

TypeScript 推論

const counter = useCounterStore();
counter.count;            // number
counter.doubled;          // number
counter.increment();      // ✅
counter.unknown;          // ❌ TS error

defineStore から完全に推論。

$reset / $patch

counter.$reset();                    // 初期状態に戻す
counter.$patch({ count: 100 });      // 複数キーを一括更新
counter.$patch((state) => {          // 関数で柔軟に
  state.count++;
  state.items.push("new");
});

DevTools

Vue DevTools の Pinia タブで:

  • 各 store の state 確認
  • 時系列 (タイムトラベル)
  • action 履歴

→ React の Redux DevTools と同等の体験。

まとめ

  • Pinia は Vue 3 公式の状態管理 (Vuex 後継)
  • defineStore("name", () => { ref/computed/function }) で定義
  • useXxxStore() で利用、storeToRefs でリアクティブ保持
  • DevTools が強力
  • TS 推論が完璧

次は Composables

考えてみよう

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

Pinia — 演習

pnpm add pinia

Lv1: 基本 — Counter store

問題

// stores/counter.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useCounterStore = defineStore("counter", () => {
  const count = ref(0);
  const doubled = computed(() => count.value * 2);
  function increment() { count.value++; }
  function reset() { count.value = 0; }
  return { count, doubled, increment, reset };
});
// main.ts
import { createPinia } from "pinia";
createApp(App).use(createPinia()).mount("#app");
<!-- どこかの component -->
<script setup>
import { useCounterStore } from "@/stores/counter.js";
import { storeToRefs } from "pinia";

const counter = useCounterStore();
const { count, doubled } = storeToRefs(counter);
const { increment, reset } = counter;
</script>

<template>
  <p>{{ count }} ({{ doubled }})</p>
  <button @click="increment">+</button>
  <button @click="reset">0</button>
</template>

合格基準

  • store が動く
  • storeToRefs を忘れない

Lv2: 応用 — 非同期 action

問題

export const useUserStore = defineStore("user", () => {
  const user = ref<any>(null);
  const loading = ref(false);

  async function fetchUser(id: number) {
    loading.value = true;
    try {
      const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
      user.value = await res.json();
    } finally {
      loading.value = false;
    }
  }

  return { user, loading, fetchUser };
});
<script setup>
const userStore = useUserStore();
const { user, loading } = storeToRefs(userStore);
const { fetchUser } = userStore;
fetchUser(1);
</script>

合格基準

  • 非同期 action が動く
  • loading state の遷移を観察

Lv3: 発展 — persist 風 (manual)

問題

import { defineStore } from "pinia";
import { ref, watch } from "vue";

export const useThemeStore = defineStore("theme", () => {
  const theme = ref<"light" | "dark">(
    (localStorage.getItem("theme") as "light" | "dark") ?? "light"
  );

  watch(theme, (v) => localStorage.setItem("theme", v));

  function toggle() {
    theme.value = theme.value === "light" ? "dark" : "light";
  }

  return { theme, toggle };
});

リロード後も theme が保持されることを確認。

合格基準

  • localStorage と同期
  • リロードで復元

提出

submission.md に3問の回答を。