Laravel Precognition + React Hook Form + Shadcn/ui で作る!リアルタイムバリデーション
こんにちは!この記事では、Laravel Precognition を使ったリアルタイムバックエンドバリデーションと、React Hook Form (RHF) および Shadcn/ui を組み合わせたモダンなフォーム実装方法について解説します。
今回、RHF と Laravel Precognitionがそれぞれのデータ管理方法が衝突したことで問題が発生しました。
Laravel Precognition は、ユーザーが入力するたびにバックエンドでバリデーションを実行し、即座にフィードバックを返す素晴らしい機能です。しかし、RHF を前提とする Shadcn/ui と連携させるには、少し工夫が必要になります。特に、「Precognition が検知したエラーを、どうやって RHF 管理下の Shadcn/ui コンポーネントに表示するか」という点がポイントです。
私が試行錯誤して解決したやり方をご紹介いたします。
ただ、最適解であるとは限らないため、あくまでやり方の一つとして参考になれば幸いです。
対象読者
- Laravel Precognition (React) を利用している、または検討している方
- React Hook Form と Shadcn/ui を使ってフォームを構築している方
- リアルタイムのバックエンドバリデーション結果をフロントエンドUIにスムーズに反映させたい方
課題: 2つのフォーム管理ライブラリをどう繋ぐか?
今回取り上げる構成では、以下のライブラリがそれぞれの役割を担います。
-
laravel-precognition-react
(usePrecognitionForm
):- バックエンド (Laravel) と通信し、リアルタイムバリデーションを実行。
- バリデーションエラー (
precognitionForm.errors
) を保持。 - フォーム全体の送信 (
precognitionForm.submit
) を担当。
-
react-hook-form
(useForm as useRHForm
):- Shadcn/ui のフォームコンポーネント (
<Form>
,<FormField>
,<FormMessage>
など) と直接連携。 - クライアントサイドでのフォームデータの状態 (
methods.watch
,methods.getValues
) を管理。 - UIコンポーネントが表示するエラー状態 (
methods.formState.errors
) を管理。
- Shadcn/ui のフォームコンポーネント (
この2つを連携させる上での課題は以下の通りです。
-
エラーの同期:
precognitionForm.errors
の内容を、Shadcn/ui が参照するmethods.formState.errors
にリアルタイムで反映させる必要がある。 -
データの同期: RHF が管理するフォームデータが変更されたら、それを
precognitionForm
に伝え、リアルタイムバリデーションをトリガーする必要がある。
useEffect
と watch
による双方向同期
解決策: useEffect
フックと RHF の watch
機能を使って解決しました。
1. エラー同期 (Precognition → RHF) - useEffect
useEffect
を使って precognitionForm.errors
オブジェクトを監視します。変更があった場合、つまり Precognition がバックエンドから新しいエラー情報を受け取った場合に、RHF の setError
メソッドを呼び出して、対応するフィールドのエラー状態 (methods.formState.errors
) を更新します。
また、Precognition 側でエラーが解消された場合(precognitionForm.errors
から特定のキーが消えた場合)に、RHF 側のエラーも clearErrors
でクリアするロジックも含めました。
2. データ同期 & バリデーショントリガー (RHF → Precognition) - watch
RHF の methods.watch
機能は、フォームの入力値が変更されるたびにコールバック関数を実行します。このコールバック内で、以下の処理を行います。
-
precognitionForm.setData(fieldName, value)
: 変更されたフィールド名と値をprecognitionForm
のデータ状態に同期させます。 -
precognitionForm.validate(fieldName)
: 変更されたフィールドに対して、Precognition のリアルタイムバリデーションをトリガーします。
これにより、ユーザーが入力するたびにバックエンドでバリデーションが実行され、その結果が useEffect
を通じて RHF のエラー状態に反映される、という流れが完成します。
実装例
import React, { useEffect, useState } from "react";
import { useForm as useRHForm } from "react-hook-form";
import { useForm as usePrecognitionForm } from "laravel-precognition-react";
import { zodResolver } from '@hookform/resolvers/zod'; // 必要であればクライアントバリデーションも
import * as z from 'zod'; // Zodスキーマ定義用
// Shadcn/ui コンポーネント (例)
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
// フォームデータの型定義 (例)
const formSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
// ... 他のフィールド
});
type FormData = z.infer<typeof formSchema>;
// Precognitionが返す可能性のあるエラーの型を定義
type PrecognitionErrors = Record<keyof FormData | string, string | string[]>; // エラーは文字列または文字列配列の場合がある
export default function MyFormComponent() {
// Precognition Form Hook
const precognitionForm = usePrecognitionForm<FormData>(
"post", // HTTPメソッド
route("your.validation.endpoint"), // バリデーション用エンドポイント
{ // 初期データ
email: "",
name: "",
// ...
}
);
// React Hook Form Hook
const methods = useRHForm<FormData>({
// resolver: zodResolver(formSchema), // クライアントバリデーションも併用する場合
defaultValues: precognitionForm.data, // 初期値をPrecognitionのデータと同期
});
const { setError, clearErrors, watch, handleSubmit, formState } = methods;
const backendErrors = precognitionForm.errors as PrecognitionErrors; // 型付け
// --- エラー同期 (Precognition -> RHF) ---
useEffect(() => {
// PrecognitionからのエラーをRHFに設定
Object.keys(backendErrors).forEach((field) => {
if (field in methods.getValues()) { // フィールドが存在するか確認
setError(field as keyof FormData, {
type: "server",
message: Array.isArray(backendErrors[field]) ? backendErrors[field][0] : backendErrors[field], // エラーメッセージを取得 (配列の場合は最初の要素)
});
} else {
// フォームフィールドに直接関連しないエラーの処理 (例: rootエラー)
setError("root.serverError", { type: "server", message: backendErrors[field].toString() });
}
});
// Precognitionで解消されたエラーをRHFからクリア
Object.keys(formState.errors).forEach((field) => {
// RHFにエラーがあるが、Precognitionにはもうエラーがない場合、それをクリア
// (ただし、クライアントバリデーションのエラーは保持されるべきなので注意が必要)
// 提供されたコードの意図に近い形: Precognition側に存在しないRHFエラーをクリア
if (!(field in backendErrors) && formState.errors[field]?.type === 'server') {
clearErrors(field as keyof FormData);
}
// もしクライアントバリデーション(zodなど)も使うなら、 'server' type のエラーのみクリアするなどの調整が必要
});
// backendErrors と RHFの関数を依存配列に追加
}, [backendErrors, setError, clearErrors, formState.errors, methods]); // formState.errorsも依存配列に入れることでクリアロジックのトリガーになる
// --- データ同期 & バリデーショントリガー (RHF -> Precognition) ---
useEffect(() => {
const subscription = watch((value, { name, type }) => {
if (name && type === 'change') { // フィールド変更時のみ
// console.log(`Field changed: ${name}, Value: ${value[name]}`); // デバッグ用ログ
precognitionForm.setData(name as keyof FormData, value[name]); // データ同期
precognitionForm.validate(name as keyof FormData); // バリデーショントリガー
}
});
return () => subscription.unsubscribe(); // コンポーネントのアンマウント時に購読解除
}, [watch, precognitionForm]); // watchとprecognitionFormを依存配列に
// --- フォーム送信処理 ---
const onSubmit = (data: FormData) => {
// handleSubmitが呼ばれる = RHFのバリデーション(あれば)は通過済み
// Precognitionに最終データをセット (watchで同期してるので不要な場合もあるが念のため)
precognitionForm.setData(data);
precognitionForm.submit({
preserveScroll: true,
onSuccess: () => {
console.log("Form submitted successfully with Precognition!");
precognitionForm.reset(); // Precognitionフォームをリセット
methods.reset(precognitionForm.data); // RHFフォームもリセット
// 完了ステップへの遷移など
},
onError: (submitErrors) => {
// 送信時にもエラーが発生する可能性がある (最終バリデーションなど)
// useEffectでハンドリングされるが、追加処理が必要な場合はここに書く
console.error("Submission failed:", submitErrors);
}
});
};
return (
// Shadcn/uiのFormコンポーネントでラップ
<Form {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* rootエラー表示エリア */}
{formState.errors.root?.serverError && (
<div className="text-red-500 text-sm">{formState.errors.root.serverError.message}</div>
)}
<FormField
control={methods.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>お名前</FormLabel>
<FormControl>
<Input placeholder="例: 山田 太郎" {...field} />
</FormControl>
<FormMessage /> {/* ここでRHF経由でPrecognitionのエラーが表示される */}
</FormItem>
)}
/>
<FormField
control={methods.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>メールアドレス</FormLabel>
<FormControl>
<Input type="email" placeholder="例: user@example.com" {...field} />
</FormControl>
<FormMessage /> {/* ここでRHF経由でPrecognitionのエラーが表示される */}
</FormItem>
)}
/>
{/* 他のフォームフィールドも同様に追加 */}
<Button type="submit" disabled={precognitionForm.processing || formState.isSubmitting}>
{precognitionForm.processing ? '送信中...' : '登録する'}
</Button>
</form>
</Form>
);
}
ポイント解説:
-
usePrecognitionForm<FormData>(...)
: Precognition フックを初期化。型引数FormData
を渡すことで、setData
,errors
などで型安全性を高めます。 -
useRHForm<FormData>(...)
: RHF フックを初期化。defaultValues
をprecognitionForm.data
と同期させます。 -
useEffect
内のsetError
: Precognition からのエラーメッセージ (backendErrors[field]
) を RHF の特定フィールドのエラーとして設定します。エラーメッセージが配列の場合があるため、最初の要素[0]
を取得するか、toString()
などで文字列化します。 -
useEffect
内のclearErrors
: RHF に存在するサーバーエラー (type: 'server'
) で、かつ現在のbackendErrors
に存在しないものをクリアします。これにより、Precognition でエラーが解消された際に UI 上のエラー表示も消えます。-
注意: クライアントサイドバリデーション(例: Zod)も併用している場合、そのエラーまで消してしまわないように、
formState.errors[field]?.type === 'server'
のような条件分岐が重要になります。
-
注意: クライアントサイドバリデーション(例: Zod)も併用している場合、そのエラーまで消してしまわないように、
-
onSubmit
内のprecognitionForm.submit()
: 最終的なフォーム送信は Precognition のsubmit
メソッドで行います。 -
<FormMessage />
: この Shadcn/ui コンポーネントが、useEffect
によって同期された RHF のエラー (formState.errors
) を自動的に拾って表示してくれます。
まとめ
laravel-precognition-react
と react-hook-form
(+ Shadcn/ui) は、それぞれ強力なフォーム管理機能を提供しますが、連携させるにはコツが必要でした。
今回紹介した useEffect
による エラー同期 と、watch
による データ同期 & バリデーショントリガー のパターンは、リアルタイムのバックエンドバリデーションと高機能な UI コンポーネントライブラリを組み合わせる際のアプローチの一つです。
Discussion