Reactで作った自前バリデーションつき入力フォームのバリデーションをuseEffectでやる

目次

はじめに

入力値のバリデーションが行われるタイミングは様々です。
システムによっても違うし、人によっても違うでしょう。

この入力欄シリーズでは、以下のタイミングが登場しました。

  • SUBMITボタンが押されたタイミング(onSubmitイベント)
  • 入力欄からフォーカスが外れたタイミング(onBlurイベント)
  • 入力値が変化したタイミング(onChangeイベント)

今回は、Reactならではのタイミングでバリデーションする方法として、useEffect を使った例をご紹介します。

実物はこちら

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

リポジトリはこちら

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

概要

useEffect はReact組み込みのフックで、副作用を扱います。
副作用は、平たく言うと「ある値が変わると他に影響が出る」といった感じでしょうか。

useEffect を使うことで「ある値が変わると◯◯」の「◯◯」の部分を処理することが可能になります。

この記事を読むことで、以下のような画面を作れるようになります。

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

  • 「名前」「かな」の二つの入力欄がある
  • 両方とも必須で、30字以内という制限がある
  • バリデーションは、画面を表示したとき、入力値が変化したときに行われる
  • エラーメッセージは、それぞれの入力欄の下に赤字で表示する
  • エラーのある入力欄は赤く表示する

実装

先にコード全容です。

'use client'

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


export default function ValidateUseEffect() {
    const [formData, setFormData] = useState<ValidateUseEffectForm>({
        fullName: '',
        fullNameKana: '',
    })
    const [errors, setErrors] = useState<ValidateError[]>([])

    useEffect(() => {
        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
        }

        setErrors([
            ...validate('fullName', formData.fullName),
            ...validate('fullNameKana', formData.fullNameKana)
        ])

    }, [formData]);

    const onSubmit = (event: FormEvent<HTMLFormElement>) => {
        event.preventDefault()

        setErrors(errors)
        if (errors.length) {
            return
        }

        alert('No validate errors!')
    }

    const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
        switch (event.target.name) {
            case 'fullName': {
                setFormData({...formData, fullName: event.target.value})
                break
            }
            case 'fullNameKana': {
                setFormData({...formData, fullNameKana: event.target.value})
                break
            }
            default:
        }
    }

    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' : ''} onChange={handleChange} />
                        </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' : ''} onChange={handleChange} />
                        </Box>
                        <Box height="1rem" sx={{color: 'red'}}>
                            {
                                getErrorMessageByPropertyName('fullNameKana')
                            }
                        </Box>
                    </Box>
                    <Box>
                        <Button type="submit" variant="contained" disabled={errors.length > 0}>SUBMIT</Button>
                    </Box>
                </form>
            </Box>
            <Box mt={2} ml={2}>
                <Box>
                    <Typography variant="body2">
                        {formData.fullNameKana !== '' && `${formData.fullNameKana}さん、こんにちは。` }
                    </Typography>
                </Box>
                <Box>
                    <Typography variant="h4">
                        {formData.fullName !== '' && `${formData.fullName}さん、こんにちは。` }
                    </Typography>
                </Box>
            </Box>
        </main>
    )
}

各部分の解説

useEffectで入力値の変化を監視する

useEffect を使うことで「ある値が変わると◯◯」の「◯◯」の部分を処理することが可能になる、と書きました。
今回の場合でいうと、「入力値が変わったらバリデーションする」ということができます。

    useEffect(() => {
        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
        }

        setErrors([
            ...validate('fullName', formData.fullName),
            ...validate('fullNameKana', formData.fullNameKana)
        ])

    }, [formData]);

ハイライトしている useEffect の第二引数に formData を配列で渡しています。
この第二引数の配列をよく依存配列と呼びます。
その名のとおりuseEffect が依存している値となり、この useEffect 内の処理は依存する formData が変化すると実行されます。

useEffect 内で宣言している処理は、これまで onBluronChange で使ってきたバリデーション処理です。

注意点としては、useEffect 内の処理は「画面表示時にも実行される」ということです。
今回の場合でいうと、この画面を表示するとまず最初にバリデーションが実行されます。

画面を表示してまだ何も操作していないのに赤字で「入力してください」と注意されるのは嫌だ、という人がいるかもしれませんね。
そのような場合は、別途対策が必要になります。

おわりに

いかがだったでしょうか?
今回は入力フォームの値を useEffect を使ってバリデーションしてみました。

この useEffect はなかなかの曲者で、ちょっと間違えると処理が無限ループしてしまうのはReactあるあるですね。
とはいえ、使わないと慣れるものも慣れません。
失敗してもいいので、まずはどんどん使ってみましょう!

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

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

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

この記事を書いた人

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

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

コメント

コメントする

目次