React+fetchAPI+LaravelでFormRequestを使うは

2024/12/24に公開

まえがき

Laravelの一部にReactを組み入れて使う場合、バリデーションにはFormRequestを使いたい。
しかし、そんなやり方は書籍には載っていないので、どうすればいいか分からず、コントローラでバリデーションしたり、最悪React側でやったりと苦肉の策を導入しがちではないでしょうか。
今回うまくいったので方法を伝授します。

困るポイント

・React側でFormに添付ファイルを動的に出し入れしたい場合などはfetchAPIなどのAjaxで行わなければいけない。
・しかしその場合、コントローラでバリデーションメッセージをReact側に渡したくても、コントローラに処理が到達する前にはじき返すので、JSON化したりする処理を書く場所がない。

やり方

FormRequestを作成する。

まずは普通の「blade+同期通信のみのシンプルなjsでsubmitする」場合と同様に、

php artisan make:request ◯◯

してカスタムRequestFormを作成します。
こんな感じで。

A01CreateRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class A01CreateRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * バリデーション失敗時の処理をカスタマイズ
     */
    protected function failedValidation(Validator $validator)
    {
        \Log::debug("▼▼▼ バリデーションエラー発生時の処理を開始します。▼▼▼");

        // JSONレスポンスを返す
        $response = response()->json([
            'isSuccess' => false,
            'message' => '入力内容にエラーがあります。',
            'errors' => $validator->errors(),
        ], 200);

        // throw new HttpResponseException($response);
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'ticket_subject' => [
                'required',
                'string',
                'max:100',
            ],
            'ticket_summary' => [
                'nullable',
                'string',
                'max:1000',
            ],
          //  〜(中略)〜
        ];
    }
}

ポイントは以下のメソッドです。
このメソッド名でオーバライドすることで、バリデーションに引っかかった時の制御に割り込めます。

protected function failedValidation(Validator $validator)

以上。
この技さえ分かればあとはjs+Reactについてググりまくればなんとかなります。
・・・といいたいところですが、もうちょっと続きまで書きます。

blade

さっきRequestFormで用意したエラー情報をBladeで受け取ります。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">

    {{-- app.jsで画面に合わせたjsのロードを切り替えるために使用 --}}
    <meta name="screen-code" content="@yield('screenCode')">

    {{-- jsで実行環境のプロジェクトルートを取得するために使用 --}}
    <meta name="project-root-path" content="{{ config('app.url') }}">
    <meta name="blade-param-json" content="{{ $blade_param }}">



    {{-- jsでAjax通信するために使用 --}}
    <meta name="csrf-token" content="{{ csrf_token() }}">

React側

今度はBladeからReactに渡します。
親コンポーネントから子コンポーネントへのバケツリレーを回避するためuseContestを利用します。

ValidationErrorsContextProvider.tsx
import React, { createContext, useContext, useEffect, useState } from "react";

// Laravelのバリデーションエラーを管理するコンテキスト
type ValidationErrorsContextType = {
    validationErrors: Record<string, string[]>;
    setValidationErrors: React.Dispatch<
        React.SetStateAction<Record<string, string[]>>
    >;
};

// Laravelのバリデーションエラーを取得してDIできるようにする。
// const ValidationErrorsContext = createContext<Record<string, string[]>>({});
const ValidationErrorsContext = createContext<
    ValidationErrorsContextType | undefined
>(undefined);

export const ValidationErrorsProvider = ({
    children,
}: {
    children: React.ReactNode;
}) => {
    const [validationErrors, setValidationErrors] = useState<
        Record<string, string[]>
    >({});

    useEffect(() => {
        const metaTag = document.querySelector(
            'meta[name="validation-errors"]'
        );
        if (metaTag) {
            const content = metaTag.getAttribute("content") || "{}";

            try {
                // JSON.parse を使ってオブジェクトに変換
                const errorsObject = JSON.parse(content);
                setValidationErrors(errorsObject);
            } catch (error) {
                console.error("Failed to parse validation errors:", error);
                setValidationErrors({});
            }
        }
    }, []);

    return (
        <ValidationErrorsContext.Provider
            value={{ validationErrors, setValidationErrors }}
        >
            {children}
        </ValidationErrorsContext.Provider>
    );
};

// コンテキストを利用するためのカスタムフック
export const useValidationErrors = () => {
    const context = useContext(ValidationErrorsContext);

    if (!context) {
        throw new Error(
            "useValidationErrors must be used within a ValidationErrorsProvider"
        );
    }

    return context; 
};

エントリポイント。こんな感じでコンテキストプロパイダーのタグで目的の親コンポーネントを囲む。

A01.tsx
import { createRoot } from "react-dom/client";
import A01Form from "../A01/A01Form";
import { ValidationErrorsProvider } from "../ContextProvider/ValidationErrorsContextProvider";
import { RouteNamesProvider } from "../ContextProvider/RouteNamesContextProvider";
import { BladeParamContextProvider } from "../ContextProvider/BladeParamContextProvider";

// ReactをBladeにマウントする。
const root = createRoot(
    document.getElementById("A01_react_app_form") as Element
);

if (root) {
    root.render(
        <RouteNamesProvider>
            <BladeParamContextProvider>
                <ValidationErrorsProvider>
                    <A01Form />
                </ValidationErrorsProvider>
            </BladeParamContextProvider>
        </RouteNamesProvider>
    );
}

ここまで準備できたら、こんな感じでバリデーション情報を表示できるようになります。

TextForm.tsx
import React, { useState } from "react";
import { useValidationErrors } from "../ContextProvider/ValidationErrorsContextProvider";

const TextForm = (props: {
    labelText: string;
    inputName: string;
    inputValue: string;
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; // 親からonChangeを受け取る
}) => {
    const { validationErrors } = useValidationErrors();

    return (
        <div className="c-form-row">
            <label className="c-form-label u-required">{props.labelText}</label>
            <input
                className="c-form-input-text u-width-per-100"
                type="text"
                name={props.inputName}
                value={props.inputValue}
                onChange={props.onChange}
            />
            {validationErrors && <p>{validationErrors[props.inputName]}</p>}
        </div>
    );
};

export default TextForm;

おわりに

これで、普通のsubmit送信と同じFormRequestを活用することができました。
Laravel+Reactの組み合わせはまだまだ情報不足ですが、Reactの機能はシンプルなものが多いので組み合わせていけば意外な使い方もできるかなと思いました。

株式会社ONE WEDGE

【Serverlessで世の中をもっと楽しく】 ONE WEDGEはServerlessシステム開発を中核技術としてWeb系システム開発、AWS/GCPを利用した業務システム・サービス開発、PWAを用いたモバイル開発、Alexaスキル開発など、元気と技術力を武器にお客様に真摯に向き合う価値創造企業です。
https://onewedge.co.jp

Discussion