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

目次

はじめに

せっかくプログラムを作るなら、いいものを作りたいですよね。

いいものを作ろうと思ったら、自ずと機能が増えていくと思います。
機能が増えていくと、今度は見やすさや直しやすさが大事になってきます。

見にくかったら直す箇所を探すだけでも大変ですし、同じようなコードがたくさんあったら直さないといけないコードもたくさんですからね。
それにコードを直すときって、だいたい作ってから数ヶ月後・数年後とかです。
そんな前に書いたものはほとんど忘れてるので、一からコードを読むのと同じです。
未来の自分のためにも、見やすく直しやすいコードを書けるようにしておきましょう!

見やすく直しやすいコードを書くために、機能が増えてきたら「同じような処理書いてないかな?」「関数とかメソッドにまとめられないかな?」という視点で見直してみましょう。

今回は、今まで作ってきた処理をカスタムフックに抽出してみました。

実物はこちら

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

リポジトリはこちら

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

概要

カスタムフックはReactの機能の一つで、state を使う処理を自分好みに関数化することができます。

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

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

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

実装

先にコード全容です。

'use client'

import {Box, Button, Typography} from "@mui/material";
import {ChangeEvent, Dispatch, FormEvent, SetStateAction, useEffect, useState} from "react";
import './style.css'

const useTextInput = (): [{ value: string, errors: string[]}, Dispatch<SetStateAction<string>>] => {
    const [value, setValue] = useState<string>('')
    const [errors, setErrors] = useState<string[]>([])

    useEffect(() => {
        const validate = (): string[] => {
            const errors: string[] = [];
            if (value === '') {
                errors.push('入力してください')
            }
            if (value.length > 30) {
                errors.push('30字以下で入力してください')
            }
            return errors
        }

        setErrors(validate())

    }, [value]);

    return [{ value, errors }, setValue]
}

const useAlphaInput = (): [{ value: string, errors: string[]}, Dispatch<SetStateAction<string>>] => {
    const [value, setValue] = useState<string>('')
    const [errors, setErrors] = useState<string[]>([])

    useEffect(() => {
        const validate = (): string[] => {
            const errors: string[] = [];
            if (value === '') {
                errors.push('入力してください')
            }
            if (value.length > 30) {
                errors.push('30字以下で入力してください')
            }
            if (!value.match(/^[a-zA-Z ]*$/)) {
                errors.push('半角英字で入力してください')
            }
            return errors
        }

        setErrors(validate())

    }, [value]);

    return [{ value, errors }, setValue]
}

export default function ValidateCustomHook() {
    const [fullName, setFullName] = useTextInput()
    const [fullNameKana, setFullNameKana] = useTextInput()
    const [fullNameEnglish, setFullNameEnglish] = useAlphaInput()

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

        if (fullName.errors.length || fullNameKana.errors.length) {
            return
        }

        alert('No validate errors!')
    }

    const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
        switch (event.target.name) {
            case 'fullName': {
                setFullName(event.target.value)
                break
            }
            case 'fullNameKana': {
                setFullNameKana(event.target.value)
                break
            }
            case 'fullNameEnglish': {
                setFullNameEnglish(event.target.value)
                break
            }
            default:
        }
    }

    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={fullName.errors.length ? 'error' : ''} onChange={handleChange} />
                        </Box>
                        <Box height="1rem" sx={{color: 'red'}}>
                            {
                                fullName.errors.map((error, index) => <span key={`${index}-fullName`}>{error}</span>)
                            }
                        </Box>
                    </Box>
                    <Box mb={2}>
                        <Box mb={1}>
                            <span>かな</span>
                        </Box>
                        <Box>
                            <input type="text" name="fullNameKana" className={fullNameKana.errors.length ? 'error' : ''} onChange={handleChange} />
                        </Box>
                        <Box height="1rem" sx={{color: 'red'}}>
                            {
                                fullNameKana.errors.map((error, index) => <span key={`${index}-fullNameKana`}>{error}</span>)
                            }
                        </Box>
                    </Box>
                    <Box mb={2}>
                        <Box mb={1}>
                            <span>Full Name</span>
                        </Box>
                        <Box>
                            <input type="text" name="fullNameEnglish" className={fullNameEnglish.errors.length ? 'error' : ''} onChange={handleChange} />
                        </Box>
                        <Box height="1rem" sx={{color: 'red'}}>
                            {
                                fullNameEnglish.errors.map((error, index) => <span key={`${index}-fullNameEnglish`}>{error}</span>)
                            }
                        </Box>
                    </Box>
                    <Box>
                        <Button type="submit" variant="contained" disabled={fullName.errors.length > 0 || fullNameKana.errors.length > 0}>SUBMIT</Button>
                    </Box>
                </form>
            </Box>
            <Box mt={2} ml={2}>
                <Box>
                    <Typography variant="body2">
                        {fullNameKana.value !== '' && `${fullNameKana.value}さん、こんにちは。` }
                    </Typography>
                </Box>
                <Box>
                    <Typography variant="h4">
                        {fullName.value !== '' && `${fullName.value}さん、こんにちは。` }
                    </Typography>
                </Box>
            </Box>
        </main>
    )
}

各部分の解説

カスタムフック useTextInput

日本語入力の処理をまとめたカスタムフックです。

入力値とエラーメッセージの state、useEffect を使ったバリデーション処理がまとめられていることを確認してください。

const useTextInput = (): [{ value: string, errors: string[]}, Dispatch<SetStateAction<string>>] => {
    const [value, setValue] = useState<string>('')
    const [errors, setErrors] = useState<string[]>([])

    useEffect(() => {
        const validate = (): string[] => {
            const errors: string[] = [];
            if (value === '') {
                errors.push('入力してください')
            }
            if (value.length > 30) {
                errors.push('30字以下で入力してください')
            }
            return errors
        }

        setErrors(validate())

    }, [value]);

    return [{ value, errors }, setValue]
}

カスタムフック useAlphaInput

半角英字入力の処理をまとめたカスタムフックです。

useTextInput とほぼ同じですが、違いは半角英字のバリデーションがある点です。

const useAlphaInput = (): [{ value: string, errors: string[]}, Dispatch<SetStateAction<string>>] => {
    const [value, setValue] = useState<string>('')
    const [errors, setErrors] = useState<string[]>([])

    useEffect(() => {
        const validate = (): string[] => {
            const errors: string[] = [];
            if (value === '') {
                errors.push('入力してください')
            }
            if (value.length > 30) {
                errors.push('30字以下で入力してください')
            }
            if (!value.match(/^[a-zA-Z ]*$/)) {
                errors.push('半角英字で入力してください')
            }
            return errors
        }

        setErrors(validate())

    }, [value]);

    return [{ value, errors }, setValue]
}

カスタムフックを使う

カスタムフックが作成できたので、早速使ってみましょう。

使い方は他のフックと同じです。今回は useState と使い方を揃えています。

ハイライトしている以下の点を確認してください。

  • カスタムフックの呼び出し方(useState と同じこと)
  • 入力値の更新の仕方(useState と同じこと)
export default function ValidateCustomHook() {
    const [fullName, setFullName] = useTextInput()
    const [fullNameKana, setFullNameKana] = useTextInput()
    const [fullNameEnglish, setFullNameEnglish] = useAlphaInput()

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

        if (fullName.errors.length || fullNameKana.errors.length) {
            return
        }

        alert('No validate errors!')
    }

    const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
        switch (event.target.name) {
            case 'fullName': {
                setFullName(event.target.value)
                break
            }
            case 'fullNameKana': {
                setFullNameKana(event.target.value)
                break
            }
            case 'fullNameEnglish': {
                setFullNameEnglish(event.target.value)
                break
            }
            default:
        }
    }

おわりに

いかがだったでしょうか?
今回は、今まで作ってきた処理をカスタムフックに抽出してみました。

useTextInputuseAlphaInput を比べてみると、中身はほぼ同じなのでまだ共通化ができそうですね。

カスタムフック内で useStateuseEffect を呼んでいるように、カスタムフック内でカスタムフックを呼ぶことももちろん可能です。
useTextInputuseAlphaInput の共通処理を、さらにカスタムフックに抽出してみるのもアリかもしれませんね。

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

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

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

この記事を書いた人

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

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

コメント

コメントする

目次