PiniaとComposition APIの使い分けで迷わない状態管理

Nuxt 3を使った開発で「困った!」と感じる場面は意外と多いものです。

このシリーズでは、現場でよく遭遇する困りごとに対して、具体的な解決方法を提供していきます。

PiniaとComposition APIの使い分けで迷わない状態管理

状態管理って悩みますよね。

特にNuxt 3では、Composition APIが標準となり、 「これってPiniaを使うべき?

それともComposition APIだけで十分?」という 判断に迷うことが多いのではないでしょうか。

今回は、よくある困りごととして「ユーザーの認証状態の管理」を例に、 PiniaとComposition APIの使い分けについて考えていきます。

Nuxt3で状態管理の使い分け、結論から!

状態管理の選択は、以下の基準で判断すると迷わなくなります:

アプリ全体で共有する状態 → Pinia

  • 複数のコンポーネントでアクセスが必要
  • ページ遷移後も状態を維持する必要がある
  • ローカルストレージとの連携が必要

コンポーネント内での一時的な状態 → Composition API

  • フォームの入力値
  • UIの表示状態(モーダルの開閉など)
  • 親子コンポーネント間での状態共有

具体例:ユーザー認証の状態管理

R
R

「ユーザーの認証状態をアプリ全体で管理したい。でも、実装が複雑になりすぎないかな…」

このような悩みの背景には、以下のような要件があります:

  • ログイン状態をアプリケーション全体で共有したい
  • ページをリロードしても状態を維持したい
  • ログアウト時は確実に状態をクリアしたい

この場合は、Piniaを使用するのがベストな選択です。 具体的な実装例を見ていきましょう。

具体例:ユーザー認証の状態管理の実装

「ユーザーの認証状態をアプリ全体で管理したい。でも、実装が複雑になりすぎないかな…」

このような悩みには、以下のような実装で対応できます:

typescript
// stores/auth.ts
import { defineStore } from 'pinia'

interface User {
  id: string
  email: string
  name: string
}

interface AuthState {
  user: User | null
  token: string | null
  isAuthenticated: boolean
}

export const useAuthStore = defineStore('auth', {
  state: (): AuthState => ({
    user: null,
    token: null,
    isAuthenticated: false
  }),

  actions: {
    setUser(user: User | null) {
      this.user = user
      this.isAuthenticated = !!user
    },

    setToken(token: string | null) {
      this.token = token
      if (token) {
        localStorage.setItem('auth_token', token)
      } else {
        localStorage.removeItem('auth_token')
      }
    },

    async login(email: string, password: string) {
      try {
        const response = await $fetch('/api/login', {
          method: 'POST',
          body: { email, password }
        })
        
        this.setToken(response.token)
        this.setUser(response.user)
        
        return { success: true }
      } catch (error) {
        return { 
          success: false, 
          error: 'ログインに失敗しました'
        }
      }
    }
  },

  getters: {
    currentUser: (state): User | null => state.user,
    isLoggedIn: (state): boolean => state.isAuthenticated
  }
})

この実装のポイントを見ていきましょう:

1. 明確な型定義

TypeScriptの型定義をしっかり行うことで、どんな情報を扱うのかが一目瞭然です。また、補完が効くためコーディングがスムーズになります。

2. 状態の一元管理

認証に関する状態をまとめて管理することで:

  • 状態の更新漏れを防げる
  • ローカルストレージとの連携も一箇所で管理できる

3. エラーハンドリング

ログイン処理のエラーハンドリングをストア内で行うことで、コンポーネント側の実装がシンプルになります。

コンポーネントでの使用例を見てみましょう:

typescript
// components/Header.vue
const Header = defineComponent({
  setup() {
    const auth = useAuthStore()
    
    return {
      isLoggedIn: computed(() => auth.isLoggedIn),
      user: computed(() => auth.currentUser),
      logout: () => auth.logout()
    }
  }
})

まとめ

今回のポイントを整理しておきましょう:

  1. PiniaとComposition APIの使い分け基準
    • Piniaは「アプリ全体で共有する状態」の管理に
    • Composition APIは「コンポーネントローカルな状態」の管理に
  2. 認証状態管理でPiniaを選ぶ理由
    • アプリ全体での状態共有が容易
    • ページリロード時の状態維持が実装しやすい
    • ストア内でロジックを完結できる
  3. 実装時の重要ポイント
    • TypeScriptの型定義をしっかり行う
    • 状態の更新は必ずアクションを経由する
    • エラーハンドリングはストア内で実装する

最後まで読んでいただき、ありがとうございました。
このシリーズが、みなさんの開発のお役に立てば幸いです。