Laravel + React で2段階「バリデーション」をつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、これは実際に私が使ってみて「うーん…」となった話なのですが、少し大きなファイルをアップロードする際に次のようになってしまいました。

ファイル以外のデータが間違っていて、また時間をかけてアップロードしなくちゃいけなくなった…😩

つまり、これは以下の流れになってしまい、また長い待ち時間が発生することになってしまった訳です…

  1. データ&ファイル送信
  2. データがバリデーションに引っかかった
  3. 入力を直してまた送信(=時間がかかる)

さすがにこれはユーザビリティとして良いことではないので、今回「2段階バリデーション」なるものを作ってみることにしました。

つまり、送信が早い「ファイル以外」の入力データだけ先にバリデーションをかけ、問題がなければファイルも合わせて送信するというものです。

※ クライアントサイドでバリデーションすればいいとも思うのですが、バリデーションは結局サーバーサイドも用意しないといけないので、どうせだったら一元管理したいわけです。

そこで❗

今回はLaravel + Reactで2段階バリデーションなるものを実装してみることにしました。

ぜひ何かの参考になりましたら嬉しいです。😊✨


「ビアバーでとなりにいた
海外の方と話してみたら、
パイロットさんで驚きました」

開発環境: Laravel 9.x、React、Inertia.js、Vite

コントローラーをつくる

では、まずはコントローラーをつくっていきます。

php artisan make:controller TwoStepsValidationController

すると、ファイルが作成されるので中身を以下のように変更します。

app/Http/Controllers/TwoStepsValidationController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;

class TwoStepsValidationController extends Controller
{
    public function create()
    {
        return Inertia::render('TwoStepsValidation/Create');
    }

    public function preStore(Request $request)
    {
        $request->validate([ // 事前バリデーション
            'title' => 'required',
            'description' => 'required',
        ]);

        return redirect()->route('two_steps_validation.create');
    }

    public function store(Request $request)
    {
        $request->validate([ // 本バリデーション
            'title' => 'required',
            'description' => 'required',
            'file' => ['required', 'file', 'max:2048'],
        ]);

        // ここで保存処理を行う

        return redirect()->route('two_steps_validation.create');
    }
}

この中でやっているのは、それぞれ以下のとおりです。

  • create() ・・・ 送信フォームの表示
  • preStore() ・・・ 事前バリデーション
  • store ・・・ 本バリデーション(&保存)

ビューをつくる

では、続いて先ほどcreate()内でセットしたビュー(テンプレート)を作成します。

resources/js/Pages/TwoStepsValidation/Create.jsx

import React, {useState, useRef} from 'react';
import {Inertia} from "@inertiajs/inertia";

export default function Create(props) {

    // Data
    const [title, setTitle] = useState('');
    const [description, setDescription] = useState('');
    const [file, setFile] = useState(null);
    const errors = props.errors;

    // Refs
    const inputFileRef = useRef();

    // Handlers
    const handleFileChange = e => {

        setFile(e.target.files[0]);

    };
    const handlePreSubmit = () => {

        const url = route('two_steps_validation.pre_store');
        const data = { // ファイル以外のデータ送信
            title,
            description,
        };

        Inertia.post(url, data, {
            onSuccess() {

                handleSubmit(); // 本送信

            }
        });

    };
    const handleSubmit = () => {

        const url = route('two_steps_validation.store');
        const data = { // 全データ送信
            title,
            description,
            file,
        };

        Inertia.post(url, data, {
            forceFormData: true, // 必ず FormData で送信
            onSuccess() {

                setTitle('');
                setDescription('');
                setFile(null);
                inputFileRef.current.value = null; // ファイル選択をクリア

                alert('完了しました');

            }
        });

    };

    return (
        <div className="w-48 p-5">
            <div className="mb-6">
                <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
                    タイトル
                </label>
                <input
                    type="text"
                    value={title}
                    className="border border-gray-300 text-gray-900 text-sm"
                    onChange={e => setTitle(e.target.value)} />
                {errors.title && <p className="text-red-500 text-xs italic">{errors.title}</p>}
            </div>
            <div className="mb-6">
                <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
                    内容
                </label>
                <textarea
                    value={description}
                    className="border border-gray-300 text-gray-900 text-sm"
                    onChange={e => setDescription(e.target.value)} />
                {errors.description && <p className="text-red-500 text-xs italic">{errors.description}</p>}
            </div>
            <div className="mb-6">
                <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
                    容量が大きいファイル
                </label>
                <input
                    ref={inputFileRef}
                    type="file"
                    onChange={e => handleFileChange(e)} />
                {errors.file && <p className="text-red-500 text-xs italic">{errors.file}</p>}
            </div>
            <button
                type="button"
                className="text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-4 py-2.5 text-center"
                onClick={() => handlePreSubmit()}>
                送信する
            </button>
        </div>
    );

}

この中で通常の重要なのが、「ファイル選択」の部分です。

というのも、ファイル選択は通常のデータバインディングとは違い、以下の作業を自分で実行する必要があるためです。

  1. 選択されたらファイルを取得&データ格納する
  2. 送信が完了したら、選択済みファイルをクリアする

なお、ファイル選択のクリアは直接<input>要素にアクセスできるようuseRef()をセットしていますが、inputFileRef.current.value = null;と、currentが必要なことに注意してください。

ルートをつくる

では、最後にルートを追加します。

routes/web.php

// 省略

use App\Http\Controllers\TwoStepsValidationController;

// 省略

Route::prefix('two_steps_validation')->controller(TwoStepsValidationController::class)->group(function(){

    Route::get('/create', 'create')->name('two_steps_validation.create');
    Route::post('/pre', 'preStore')->name('two_steps_validation.pre_store');
    Route::post('/', 'store')->name('two_steps_validation.store');

});

これで作業は完了です。
お疲れ様でした✨😊👍

テストしてみる

では、実際にテストしてみましょう❗

Viteを起動後、ブラウザで、「http://******/two_steps_validation/create」へアクセスします。

すると、以下のようなフォームが表示されるので、まずは何も入力せず「送信する」ボタンをクリックしてみましょう。

すると・・・・・・

はい❗
2段階でのバリデーションですので、まずは「タイトル」と「内容」の2つだけにエラーが表示されました。

では、次に「タイトル」と「内容」は入力しますが、ファイルは空のままで送信してみましょう。

どうなるでしょうか・・・・・・

はい❗
先ほどはバリデーション・チェックされなかったファイルにエラーが表示されました。

成功ですね😊✨

では、折角ですので、ファイルを選択して送信してみましょう。

うまくいくでしょうか・・・・・・???

はい❗
完了を知らせるアラートが表示されて、さらにフォームが初期状態に戻りました。

すべて成功です😊✨

企業様へのご提案

今回のようにサイズの大きなファイルを送信する場合は、バリデーションを分割することでユーザビリティを向上させることができます。

このような「ちょっとした配慮」をウェブサイトやシステムに組み込むことでより使いやすくなり、作業効率や満足度をあげることができます。

もしそういった改善をご希望でしたら、お問い合わせからお気軽にご連絡ください。

お待ちしております。m(_ _)m

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はLaravel + Reactを使った「2段階バリデーション」を実装してみました。

この記事で直近のReact記事は4つめですが、refなどVueと共通する部分が多いので、結構すんなりと実装できホッとしています(正直Reactはマシでしたが、その昔のAngularの苦手意識が蘇ってきたんですよね…😂)

やはり設計思想は違えど、向かっている方向は同じということなのでしょうか。

今後も開発の世界から目が離せませんね。

ではでは〜❗

「この前、国産ビール発祥の地
(北新地)に行ってきました🍺」

このエントリーをはてなブックマークに追加       follow us in feedly