Reactで入力フォームを作り、自前でバリデーションする

目次

はじめに

「ちょっとした入力フォームを作りたいんだけど、バリデーションってどうやってやるんだろう?」
「ライブラリ入れるほどでもないんだけどな・・・」

この記事では、Reactで自前のバリデーション機能を組み込んだ入力フォームの作り方をシェアします。

実物はこちら

あわせて読みたい
React Recipe Reactアプリのレシピ集

リポジトリはこちら

GitHub
GitHub - hakoratory/react-recipe Contribute to hakoratory/react-recipe development by creating an account on GitHub.

概要

この記事を読むことで、以下のような画面を作れるようになります。
※SUBMITボタンの下に表示しているものはデバッグ用です。

以下の仕様を想定して作っています。

  • 「名前」「かな」の二つの入力欄がある
  • 両方とも必須で、30字以内という制限がある
  • バリデーションは、入力欄からフォーカスが外れたとき、SUBMITボタンが押されたときに行われる
  • エラーメッセージは、それぞれの入力欄の下に赤字で表示する
  • エラーのある入力欄は赤く表示する

実装

先にコード全容です。

'use client'

import {Box, Button} from "@mui/material";
import {ChangeEvent, FormEvent, useState} from "react";
import './style.css'
import {ValidateError} from "@/types/validate-scratch";


export default function ValidateScratch() {
    const [errors, setErrors] = useState<ValidateError[]>([]);

    const onSubmit = (event: FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const form = new FormData(event.currentTarget);

        const errors = validateForm(form)
        setErrors(errors)
        if (errors.length) {
            return
        }

        alert('No validate errors!')
    }

    const handleBlur = (event: ChangeEvent<HTMLInputElement>) => {
        event.preventDefault();

        const errorsWithoutTarget = errors.filter(error => error.key !== event.target.name)
        const newErrors = errorsWithoutTarget.concat(validate(event.target.name, event.target.value))
        setErrors(newErrors)
    }

    const validateForm = (form: FormData): ValidateError[] => {
        return [
            ...validate('fullName', form.get("fullName") as string ?? ''),
            ...validate('fullNameKana', form.get("fullNameKana") as string ?? '')
        ]
    }

    const validate = (propertyName: string, value: string): ValidateError[] => {
        const errors: ValidateError[] = [];
        if (value === '') {
            errors.push({
                key: propertyName,
                message: '名前を入力してください'
            })
        }
        if ((value as string).length > 30) {
            errors.push({
                key: propertyName,
                message: '30字以下で入力してください'
            })
        }
        return errors
    }

    const getErrorByPropertyName = (propertyName: string) => {
        return errors.filter(error => error.key === propertyName)
    }

    const getErrorMessageByPropertyName = (propertyName: string) => {
        return getErrorByPropertyName(propertyName).map((error, index) => <span key={`${index}-${error.key}`}>{error.message}</span>)
    }

    const isError = (propertyName: string) => {
        return getErrorByPropertyName(propertyName).length > 0
    }

    return (
        <main>
            <Box mt={2} ml={2}>
                <form onSubmit={onSubmit}>
                    <Box mb={2}>
                        <Box mb={1}>
                            <span>名前</span>
                        </Box>
                        <Box>
                            <input type="text" name="fullName" className={isError('fullName') ? 'error' : ''} onBlur={handleBlur} />
                        </Box>
                        <Box height="1rem" sx={{color: 'red'}}>
                            {
                                getErrorMessageByPropertyName('fullName')
                            }
                        </Box>
                    </Box>
                    <Box mb={2}>
                        <Box mb={1}>
                            <span>かな</span>
                        </Box>
                        <Box>
                            <input type="text" name="fullNameKana" className={isError('fullNameKana') ? 'error' : ''} onBlur={handleBlur} />
                        </Box>
                        <Box height="1rem" sx={{color: 'red'}}>
                            {
                                getErrorMessageByPropertyName('fullNameKana')
                            }
                        </Box>
                    </Box>
                    <Box>
                        <Button type="submit" variant="contained">SUBMIT</Button>
                    </Box>
                </form>
                <Box>
                    {errors[0]?.key} {errors[0]?.message}
                </Box>
                <Box>
                    {errors[1]?.key} {errors[1]?.message}
                </Box>
            </Box>
        </main>
)
}

各部分の解説

入力欄

項目名と入力欄がペアになっていて下部にボタンがある、よく見る形のUIかと思います。

                <form onSubmit={onSubmit}>
                    <Box mb={2}>
                        <Box mb={1}>
                            <span>名前</span>
                        </Box>
                        <Box>
                            <input type="text" name="fullName" className={isError('fullName') ? 'error' : ''} onBlur={handleBlur} />
                        </Box>
                        <Box height="1rem" sx={{color: 'red'}}>
                            {
                                getErrorMessageByPropertyName('fullName')
                            }
                        </Box>
                    </Box>
                    <Box mb={2}>
                        <Box mb={1}>
                            <span>かな</span>
                        </Box>
                        <Box>
                            <input type="text" name="fullNameKana" className={isError('fullNameKana') ? 'error' : ''} onBlur={handleBlur} />
                        </Box>
                        <Box height="1rem" sx={{color: 'red'}}>
                            {
                                getErrorMessageByPropertyName('fullNameKana')
                            }
                        </Box>
                    </Box>
                    <Box>
                        <Button type="submit" variant="contained">SUBMIT</Button>
                    </Box>
                </form>

ハイライトしている7行目と11行目をご覧ください。

7行目は入力欄を出力する input タグです。
エラーがあるときは class="error" が設定されること、フォーカス時(onBlur)に処理が実行されることをご確認ください。

11行目はエラーメッセージを出力する関数です。
引数で項目名を指定していることをご確認ください。

エラーの state と型定義

ValidateScratch関数の冒頭で、エラーの状態を管理する state を宣言しています。

export default function ValidateScratch() {
    const [errors, setErrors] = useState<ValidateError[]>([]);

errors の型は、プロパティにkeymessage を持つ ValidateError 型の配列です。

export type ValidateError = {
    key: string,
    message: string,
}

サブミット時とフォーカスアウト時にバリデーションを実行する

仕様にあるとおり、バリデーションは入力欄からフォーカスが外れたとき、SUBMITボタンが押されたときに行われます。

    const onSubmit = (event: FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const form = new FormData(event.currentTarget);

        const errors = validateForm(form)
        setErrors(errors)
        if (errors.length) {
            return
        }

        alert('No validate errors!')
    }

    const handleBlur = (event: ChangeEvent<HTMLInputElement>) => {
        event.preventDefault();

        const errorsWithoutTarget = errors.filter(error => error.key !== event.target.name)
        const newErrors = errorsWithoutTarget.concat(validate(event.target.name, event.target.value))
        setErrors(newErrors)
    }

フォーム全体をバリデートする validateForm 関数と、入力欄ごとにバリデートする validate 関数があることをご確認ください。

handleBlur 関数の以下の処理は、バリデーションエラーをセットしています。
二つあるうちのもう一方の入力欄でエラーが起きている場合、そのエラーを消してしまわないよう filter でエラーを退避しています。
退避したエラーに、新しいバリデーションエラーを結合している(concat している)形ですね。

        const errorsWithoutTarget = errors.filter(error => error.key !== event.target.name)
        const newErrors = errorsWithoutTarget.concat(validate(event.target.name, event.target.value))
        setErrors(newErrors)

バリデーション処理

肝となるバリデーション処理です。

validateForm 関数は、各項目のバリデーションエラーを結合しているだけです。
スプレッド構文 ... をうまく使うとコードがスッキリするのでぜひマスターしましょう。

validate 関数は、引数で項目名と値を受け取ってバリデートします。
値が未入力のとき、値が30字を超えているときにバリデーションエラーとなります。

    const validateForm = (form: FormData): ValidateError[] => {
        return [
            ...validate('fullName', form.get("fullName") as string ?? ''),
            ...validate('fullNameKana', form.get("fullNameKana") as string ?? '')
        ]
    }

    const validate = (propertyName: string, value: string): ValidateError[] => {
        const errors: ValidateError[] = [];
        if (value === '') {
            errors.push({
                key: propertyName,
                message: '名前を入力してください'
            })
        }
        if ((value as string).length > 30) {
            errors.push({
                key: propertyName,
                message: '30字以下で入力してください'
            })
        }
        return errors
    }

エラー表示とエラー判定

エラーの state には、「名前」と「かな」両方のエラーが入ります。
そのため、項目名を指定してエラー表示とエラー判定を行う仕組みにしています。

getErrorByPropertyName 関数は、指定した項目のエラーオブジェクトを配列で返します。
getErrorMessageByPropertyName 関数は、指定した項目のエラーメッセージを配列で返します。
isError 関数は、指定した項目にエラーがあるかを判定します。

    const getErrorByPropertyName = (propertyName: string) => {
        return errors.filter(error => error.key === propertyName)
    }

    const getErrorMessageByPropertyName = (propertyName: string) => {
        return getErrorByPropertyName(propertyName).map((error, index) => <span key={`${index}-${error.key}`}>{error.message}</span>)
    }

    const isError = (propertyName: string) => {
        return getErrorByPropertyName(propertyName).length > 0
    }

おわりに

いかがだったでしょうか?
今回は自前のバリデーション機能を組み込んだ入力フォームを作ってみました。

validate 関数は文字列専用で必須固定、桁数も30桁固定になっています。
実際の画面では、もっと柔軟なバリデーションが求められると思います。

やり方はいろいろあると思うので、チャレンジしてみたい方はぜひ作ってみてくださいね。

ハコラトリは、Reactを習得したい駆け出しエンジニアを応援しています。

Reactレシピでは、これからもReactで色々なUIを作って紹介していきますので、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

28歳のときに音楽家からエンジニアに転向。
WEB系のシステム開発(Java, C#, Vue, React など)に携わっています。

自分らしく力を発揮できて、自信を持って生きられる人を増やすための活動をしています。

コメント

コメントする

目次