はじめに
Vueで状態管理する時ってリアクティブなデータを使う以外に何があるんだ?と思ったので調べてみました。
Vue(※Vue2)ではPiniaという状態管理ライブラリとの相性が良さそうでした🍍
※正しい読み方は「ピーニャ」です(ピニアと呼んでしまってました🙈)
※Vue3と当初書いてましたが、実際に使ったのはVue2でした🙇
Piniaの特徴とメリット
- シンプルで軽量なAPI:Vuexに比べて、より簡潔なコードで状態を定義・管理することができます
- 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 |
実現したいこと
ユーザー情報編集ページで、サーバー側のバリデーションのエラーが発生した場合に、メールアドレスのフィールド下へエラーメッセージを表示する。
要件によりますが、フロント側とサーバー側の両方でバリデーションを実装することが多いです。
バックエンドでのバリデーションは、万が一フロントエンドのバリデーションを通過して不正なデータが送信された場合のセキュリティ対策です。
処理の流れ
- フロント側から「test@」などのような値を入力してリクエストを送る
- サーバー側からステータスコード422とエラーメッセージをJSONで返す
- フロント側のプラグイン内でaxiosライブラリを使ってAPIからエラーを受け取る
- piniaの状態管理で、APIから取得したエラーメッセージに値を更新する(初期値だとエラーメッセージは空の配列に設定している)
- ユーザー情報を入力するコンポーネントで項番4で更新された値を取得する
-
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
で定義されたストアインスタンス(ストア定義から生成された、状態管理をするオブジェクト)にあたります
ストア定義とストアインスタンス違い
区分 | ストア定義 | ストアインスタンス |
---|---|---|
性質 | 設計図 | 実体 |
内容 | 状態の定義、アクションの定義 | 特定の時点での状態 |
ライフサイクル | アプリケーション全体で共通 | 各コンポーネントで生成される |
プラグイン
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です。スタイルを反映する範囲を対象のコンポーネント内に限定します。これによって他のコンポーネントとのクラス名の衝突などを防ぐことができます
バリデーションエラー発生時の画面
以下はメールアドレスの例です
まとめ
VueでPiniaを使うことにより、アプリケーションの状態を一元的に管理できる。
複数のコンポーネント間で同じ状態が共有できる。
ライブラリを使えるようになると、実装の幅が広がりますね。