26
7

【Vue】Piniaを使った状態管理

Last updated at Posted at 2024-08-31

はじめに

Vueで状態管理する時ってリアクティブなデータを使う以外に何があるんだ?と思ったので調べてみました。
Vue(※Vue2)ではPiniaという状態管理ライブラリとの相性が良さそうでした🍍

※正しい読み方は「ピーニャ」です(ピニアと呼んでしまってました🙈)
※Vue3と当初書いてましたが、実際に使ったのはVue2でした🙇

Piniaの特徴とメリット

  • シンプルで軽量なAPIVuexに比べて、より簡潔なコードで状態を定義・管理することができます
  • TypeScriptとの高い親和性:TypeScriptとの連携がスムーズで、型安全な開発が可能です
  • モジュール化::ストアをモジュール化することで、必要な部分だけをインポートし、不要な部分をロードしないようにできます

状態管理とは?

アプリケーションの状態をデータとして扱い、データの変更を一元的に管理することです。

Webアプリケーションにおいては、以下のような状態があります。
こうした状態を表現するデータを扱います。

状態 具体例
UIの状態 モーダル表示状態やタブ選択状態
DB からのデータ ブログの記事データ、商品データ
通信処理の状態 あるデータの読み込み中
ナビゲーションの状態 アプリケーションのどの位置にいるのか

フロントエンドの「状態」の入門より

実装例

開発環境

言語(FWなど) バージョン
Ruby 3.0.3
Rails 6.1.4
Vue 2.6.14
nuxt-edge latest
pinia 2.2.2

実現したいこと

ユーザー情報編集ページで、サーバー側のバリデーションのエラーが発生した場合に、メールアドレスのフィールド下へエラーメッセージを表示する。

要件によりますが、フロント側とサーバー側の両方でバリデーションを実装することが多いです。
バックエンドでのバリデーションは、万が一フロントエンドのバリデーションを通過して不正なデータが送信された場合のセキュリティ対策です。

処理の流れ

  1. フロント側から「test@」などのような値を入力してリクエストを送る
  2. サーバー側からステータスコード422とエラーメッセージをJSONで返す
  3. フロント側のプラグイン内でaxiosライブラリを使ってAPIからエラーを受け取る
  4. piniaの状態管理で、APIから取得したエラーメッセージに値を更新する(初期値だとエラーメッセージは空の配列に設定している)
  5. ユーザー情報を入力するコンポーネントで項番4で更新された値を取得する
  6. computedプロパティを使い、取得したエラーメッセージの値に変更して表示する
  • 入力例は以下になります

test@

バックエンド側

Usersコントローラー

app/controllers/api/users_controller.rb
  def update
    if @user.update!(user_params)
      render json: :ok
    else
      render json: { errors: @user.error.full_messages }, status: 422
    end
  end
  • 更新失敗した時にエラーメッセージを配列で取得し、JSON形式でフロント側に返します

Userモデル

app/models/user.rb
class User < ApplicationRecord
 
 VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email,
            presence: true,
            format: { with: VALID_EMAIL_REGEX },
            uniqueness: true

end
  • presence: true: メールアドレスが入力されているかを検証
  • format: { with: VALID_EMAIL_REGEX }: 正規表現と値がマッチするかどうかを検証
  • uniqueness: メールアドレスが一意であるかどうかを検証

ja.yml

config/locales/ja.yml
ja:
  activerecord:
    attributes:
      user:
        email: 'メールアドレス'
  errors:
    format: "%{attribute}"
    messages:
      blank: "%{attribute}を入力してください"
      invalid: "%{attribute}の形式が正しくありません"
      taken: "%{attribute}は既に存在します"
  • エラーメッセージの内容を定義します
  • %{attribute}にattributesで定義したemailプロパティの値を埋め込みます。

フロントエンド側

設定ファイル

nuxt.config.js
import { defineNuxtConfig } from '@nuxt/bridge'
export default defineNuxtConfig({
  modules: ['@nuxtjs/axios',  '@pinia'],
  provide: {
    pinia: (app) => {
      const pinia = createPinia()
      app.use(pinia)
      return pinia
    }
   },
})

こちらはnuxt.jsの設定ファイルです。

  • defineNuxtConfig: 設定が書かれたオブジェクトを含むdefineNuxtConfig関数としてエクスポートします
  • modules: Nuxt.jsの機能を拡張するモジュールを導入します
  • provide: コンポーネントに注入するためのグローバル依存関係を定義します。今回で言うと、エラーメッセージの状態をコンポーネント全体で呼び出せるようにするために設定しています
  • createPinia: Piniaインスタンスを作成します
  • app.use(pinia): 作成したPiniaインスタンスをアプリケーション全体で使用できるようにします

ストア

store/errors/errorStore.ts
import { defineStore } from 'pinia'

export const useErrorStore = defineStore('errors',  {
  state: () => ({
    errors: []
  }),
  actions: {
    setErrors(msg: []) {
      this.errors = msg;
    }
  }
})
  • defineStore: 状態管理を行うためのストアを定義します。第1引数にはストアを識別する一意な名前(ID)を定義します。第2引数にストアを定義します
  • state: 初期状態を返すための関数です。今回はエラーメッセージの初期値を空の配列で定義しています
  • actions: コンポーネントのメソッドに相当します。APIからエラーメッセージの配列を引数としてsetErrors関数で受け取ります。受け取ったエラーメッセージの値でerrorsを更新します
  • this: 関数が実行される時点での「自分自身」を指します。つまり、関数が属するオブジェクトを指す。今回の場合は、useErrorStoreで定義されたストアインスタンス(ストア定義から生成された、状態管理をするオブジェクト)にあたります

ストア定義とストアインスタンス違い

区分 ストア定義 ストアインスタンス
性質 設計図 実体
内容 状態の定義、アクションの定義 特定の時点での状態
ライフサイクル アプリケーション全体で共通 各コンポーネントで生成される

【IT用語】インスタンスとは?現実世界のモノに例えて分かりやすく解説!

プラグイン

plugins/axios.ts
import { AxiosError } from 'axios'
import { defineNuxtPlugin, useRouter, useNuxtApp } from '#app'
import { useErrorStore } from '@/store/errors/errorStore'

export default defineNuxtPlugin(() => {
  const router = useRouter()
  const app = useNuxtApp()
  app.$axios.onError((error: AxiosError) => {
    const code = error?.response?.status
    const errors = error?.response?.data?.errors
    if (code === 422) {
      const pinia = app.provide('pinia', errors)
      const errorStore = useErrorStore(pinia)
      errorStore.setErrors(errors)
      return code
    }
  })
})
  • defineNuxtPlugin: Nuxt3は標準ではdefineNuxtPlugin内で実装します
  • 作成したerrorStore.tsファイルをインポートします
  • app.$axios: グローバルに提供されるAxiosインスタンスです。アプリケーション内のどこからでもアクセスし、HTTPリクエストを行えます
  • .onError((error: AxiosError): Axiosインスタンスで発生した全てのHTTPリクエストのエラーを捕捉するためのコールバック関数です。エラーが発生すると、このコールバック関数が呼び出され、AxiosErrorオブジェクトが渡されます。この時に引数errorの値がAxiosError型であるかを判定します
  • app.provide('pinia', errors): アプリケーション内のすべての子孫コンポーネントにPiniaインスタンスのerrorsの初期状態の値を提供します
  • useErrorStore(pinia): 引数piniaがこの関数に渡されてストアが作成されます
  • errorStore.setErrors(errors): APIから取得したerrorsが引数で渡されて、ストアの状態が更新されます

フォームコンポーネント

components/molecules/userForm.vue
<template>
 <div v-if="errorMessage.length > 0">
   <p class="error-message">{{ errorMessage }}</p>
 </div>
</template>

<script lang="ts">
  import { defineComponent, ref, computed } from '@vue/composition-api'
  import { createPinia } from 'pinia'
  import { useErrorStore } from '~/store/errors/errorStore'

  export default defineComponent({

  setup() {
    const errorMessages = computed(() => errorStore.errors);

    const errorMessage = computed(() => {
      const errors = errorMessages.value.filter((msg: string) => msg.includes("メールアドレス"))
      return errors.length > 0 ? errors[0] : '';
    });

    return {
      errorMessage,
    }
  }
</script>

<style scoped>
  .error-message {
    color: red;
  }
</style>
  • computed(() => errorStore.errors): errorsの状態が更新されると新しい配列の値に変更されます
  • errorMessages.value.filter: 指定された条件に該当する新しい配列を作成します。これは各フィールドごとに個別のエラーメッセージを表示するために使っています
  • msg.includes("メールアドレス"): メールアドレスという文字列を含む要素が配列にあるかどうかをtrueまたはfalseで返します。
  • v-if="errorMessage.length > 0": エラーメッセージの配列の要素数が1以上の場合に表示します(でないと空の配列でも[]が画面上で常に表示されてしまうため)
  • {{ errorMessage }}: マスタッシュ構文でerrorMessage変数の値を表示します
  • style scoped: スコープ付きCSSです。スタイルを反映する範囲を対象のコンポーネント内に限定します。これによって他のコンポーネントとのクラス名の衝突などを防ぐことができます

バリデーションエラー発生時の画面

以下はメールアドレスの例です

スクリーンショット 2024-08-26 7.06.35.png

まとめ

VueでPiniaを使うことにより、アプリケーションの状態を一元的に管理できる。
複数のコンポーネント間で同じ状態が共有できる。
ライブラリを使えるようになると、実装の幅が広がりますね。

参考記事

26
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
7