はじめに
Reactの醍醐味といえば、コンポーネントですよね。
同じコードを何度も書いていたら、コンポーネントにまとめましょう。
そうしないと修正が何箇所にも及び、時間がかかる上にミスも増えます。
未来の自分のために、コンポーネント化しましょう。
というわけで、今回は自前バリデーションつき入力フォームのJSXをコンポーネント化してみました。
実物はこちら
リポジトリはこちら
概要
この記事を読むことで、以下のような画面が作れるようになります。
実装
先にコード全容です。
'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を作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。
コメント