Reactで作った自前バリデーションつき入力フォームのJSXをコンポーネント化する

目次

はじめに

Reactの醍醐味といえば、コンポーネントですよね。
同じコードを何度も書いていたら、コンポーネントにまとめましょう。
そうしないと修正が何箇所にも及び、時間がかかる上にミスも増えます。

未来の自分のために、コンポーネント化しましょう。

というわけで、今回は自前バリデーションつき入力フォームのJSXをコンポーネント化してみました。

実物はこちら

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

リポジトリはこちら

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

概要

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

実装

先にコード全容です。

'use client'

import {Box, Button, Typography} from "@mui/material";
import {ChangeEventHandler, Dispatch, FormEvent, SetStateAction, useEffect, useState} from "react";
import './style.css'
import {ValidateRule, InputType} from "@/types/validate-custom-hook";

const defaultRule: ValidateRule = {
    required: true,
    maxLength: 30,
}

const kanaRule: ValidateRule = {
    required: false,
    maxLength: 50,
}

const useValidate = (value: string, type: InputType, rule: ValidateRule) => {
    const [errors, setErrors] = useState<string[]>([])

    useEffect(() => {
        const validate = (): string[] => {
            const errors: string[] = [];
            if (rule.required && value === '') {
                errors.push('入力してください')
            }
            if (value.length > rule.maxLength) {
                errors.push(`${rule.maxLength}字以下で入力してください`)
            }
            if (type === 'alpha' && !value.match(/^[a-zA-Z ]*$/)) {
                errors.push('半角英字で入力してください')
            }
            return errors
        }
        setErrors(validate())
        // rule を依存関係に含めていないため警告が出るが、含めたくないので抑止する。
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [value]);

    return errors
}

const useTextInput = (rule: ValidateRule = defaultRule): [{ value: string, errors: string[]}, Dispatch<SetStateAction<string>>] => {
    const [value, setValue] = useState<string>('')
    const errors = useValidate(value, 'text', rule)

    return [{ value, errors }, setValue]
}

const useAlphaInput = (rule: ValidateRule = defaultRule): [{ value: string, errors: string[]}, Dispatch<SetStateAction<string>>] => {
    const [value, setValue] = useState<string>('')
    const errors = useValidate(value, 'alpha', rule)

    return [{ value, errors }, setValue]
}

type InputProps = {
    label: string,
    onChange: ChangeEventHandler<HTMLInputElement>,
    errors: string[]
}

const Input = ({label, errors, onChange}: InputProps) => {
    return (
        <Box mb={2}>
            <Box mb={1}>
                <span>{label}</span>
            </Box>
            <Box>
                <input type="text" className={errors.length ? 'error' : ''} onChange={onChange} />
            </Box>
            <Box height="1rem" sx={{color: 'red'}}>
                {
                    errors.map((error, index) => <span key={index}>{error}</span>)
                }
            </Box>
        </Box>
    )
}

export default function ValidateComponent() {
    const [fullName, setFullName] = useTextInput()
    const [fullNameKana, setFullNameKana] = useTextInput(kanaRule)
    const [fullNameEnglish, setFullNameEnglish] = useAlphaInput({ required: true, maxLength: 40 })

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

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

        alert('No validate errors!')
    }

    return (
        <main>
            <Box mt={2} ml={2}>
                <form onSubmit={onSubmit}>
                    <Input label="名前" onChange={(e) => setFullName(e.target.value)} errors={fullName.errors}/>
                    <Input label="かな" onChange={(e) => setFullNameKana(e.target.value)} errors={fullNameKana.errors}/>
                    <Input label="Full Name" onChange={(e) => setFullNameEnglish(e.target.value)} errors={fullNameEnglish.errors}/>
                    <Box>
                        <Button type="submit" variant="contained" disabled={fullName.errors.length > 0 || fullNameKana.errors.length > 0 || fullNameEnglish.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>
                    <Typography variant="body1">
                        {fullNameEnglish.value !== '' && `Hello ${fullNameEnglish.value}.` }
                    </Typography>
                </Box>
            </Box>
        </main>
    )
}

各部分の解説

コンポーネント化の解説の前に、コンポーネント化する前のJSXを載せておきます。
ハイライトした部分が入力欄になりますが、同じようなコードが続いているのがわかると思います。
これらをコンポーネント化していきます。

    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 || fullNameEnglish.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>
    )
}

入力欄のコンポーネント Input

入力欄は、項目名(名前、かな、Full Name)と入力フォーム(input)、そしてエラー表示の3つの要素でできています。
これらをごっそりコピペして Input コンポーネントとして抽出しました。

特に変わったことはしておらず、素直に必要な値を props で受け取るように変更しています。

type InputProps = {
    label: string,
    onChange: ChangeEventHandler<HTMLInputElement>,
    errors: string[]
}

const Input = ({label, errors, onChange}: InputProps) => {
    return (
        <Box mb={2}>
            <Box mb={1}>
                <span>{label}</span>
            </Box>
            <Box>
                <input type="text" className={errors.length ? 'error' : ''} onChange={onChange} />
            </Box>
            <Box height="1rem" sx={{color: 'red'}}>
                {
                    errors.map((error, index) => <span key={index}>{error}</span>)
                }
            </Box>
        </Box>
    )
}

Input コンポーネントを使う

コンポーネント化できたので早速使ってみましょう。
何十行もあったコードが 3行にまとまっていることを確認してください。

    return (
        <main>
            <Box mt={2} ml={2}>
                <form onSubmit={onSubmit}>
                    <Input label="名前" onChange={(e) => setFullName(e.target.value)} errors={fullName.errors}/>
                    <Input label="かな" onChange={(e) => setFullNameKana(e.target.value)} errors={fullNameKana.errors}/>
                    <Input label="Full Name" onChange={(e) => setFullNameEnglish(e.target.value)} errors={fullNameEnglish.errors}/>
                    <Box>
                        <Button type="submit" variant="contained" disabled={fullName.errors.length > 0 || fullNameKana.errors.length > 0 || fullNameEnglish.errors.length > 0}>SUBMIT</Button>
                    </Box>
                </form>

おわりに

いかがだったでしょうか?
今回は、JSXの冗長だった箇所をコンポーネント化してみました。

ロジックをカスタムフックに抽出して、JSXもコンポーネント化して、だいぶいい感じのコードになったと思います。

皆さんも、ぜひたくさんコンポーネントを作ってみてください。

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

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

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

この記事を書いた人

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

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

コメント

コメントする

目次