経緯
現在従事しているプロジェクトにて、フォームのUIを実装しています。今まであまり触れてこなかったバリデーションライブラリと、react-hook-form
を使ったので備忘録です。
個人実装と違い、色々な画面に沢山のフォームを使うのでバリデーションのSchema
を一つのファイルにまとめて置きたいのと、useForm
をカスタムフック化することでコードを簡潔にし、再利用性を高めたかったので、TextField
(MUIのコンポーネントでinputタグに相当する)をバリデーション用にコンポーネント化したものと、useForm
を実装しました。
調べてもSchema
を別管理にしている例があまり見つからなかったので、型付けに苦労しましたが、スッキリと書けてSchemaの管理も効率的になるかなと思います。
react-hook-form
以外は初めて使ったので良い勉強になりました!
前提条件
本記事の対象
- 駆け出しエンジニア
- カスタムフックについて基本を理解している
-
Next.jsの
app router
について基本を理解している - Next.jsとTypeScriptでの開発ができる
必要環境とライブラリ
以下の環境とライブラリのバージョンがプロジェクトにインストールされていることを確認してください。zod
に関しては最新だとエラーがでてしまいますので、インストール時にバージョン指定が必要です。
-
zod
:3.21.x
※2023/12/18日時点で3.22.xだとreact-hook-formと一緒に使うとタイプエラーが出てしまいます。最終的に3.21にダウングレードしたら解決しました。
-
react-hook-form
:7.49.2
-
react
:18.x
-
next
:14.0.4
-
typescript
:5.x
-
@mui/material
:5.15.0
各ライブラリの詳細な概要と役割
zod
: 効果的なデータバリデーション
zod
は、TypeScriptとJavaScriptのためのバリデーションライブラリです。このライブラリの主な目的は、入力データが特定のスキーマに適合するかどうかをチェックすることです。これにより、フォームやAPIから受け取るデータが予期した形式かどうかを保証できます。
- 使用例: メールアドレスやパスワードなどのフォーム入力が適切な形式を持っているかを確認する。
- メリット: コードベースで明確なバリデーションルールを設定でき、エラーメッセージの管理も容易になります。
react-hook-form
: 効率的なフォーム管理
react-hook-form
は、Reactアプリケーション内でフォームの状態管理とバリデーションを簡素化します。パフォーマンスを重視して設計されており、不要なレンダリングを最小限に抑えることができます。
- 使用例: フォームの入力値を追跡し、ユーザーの操作に基づいてバリデーションの結果を反映する。
- メリット: フォームの処理を効率化し、開発者がフォームの状態管理に費やす時間を大幅に削減できます。
@mui/material
: 魅力的なUIコンポーネント
@mui/material
は、マテリアルデザインを基にしたReactコンポーネントライブラリです。一貫性のあるデザイン言語を提供し、使いやすいUIを迅速に構築できます。
- 使用例: ボタン、テキストフィールド、ダイアログボックスなどのUI要素をプロジェクトに迅速に組み込む。
- メリット: デザインの一貫性が保たれ、ユーザーに親しみやすいインターフェイスを提供できます。また、カスタマイズも容易で、さまざまなデザインニーズに対応可能です。
成果物(メールアドレスが一致するかを検証するバリデーション)
GitHubリポジトリ
本記事の目標成果物
- MUIの
TextField
をバリデーション用にコンポーネント化します。 -
useForm
をzod
と結合し、schema
を別ファイルにまとめて見通しのよい構成で再利用性の高いカスタムフックを作成します。
実装
環境構築
-
コマンドから対話式の設定をして、Next.jsのプロジェクトを作成。
npx next-create-app@latest Need to install the following packages: create-next-app@14.0.4 Ok to proceed? (y) y √ What is your project named? ... sample-validation √ Would you like to use TypeScript? ... No / Yes √ Would you like to use ESLint? ... No / Yes √ Would you like to use Tailwind CSS? ... No / Yes √ Would you like to use `src/` directory? ... No / Yes √ Would you like to use App Router? (recommended) ... No / Yes √ Would you like to customize the default import alias (@/*)? ... No / Yes Creating a new Next.js app in C:\Users\yuu-mem11\workSpace\sample-validation.
-
必要なライブラリをインストール
npm install @mui/material @emotion/react @emotion/styled @hookform/resolvers react-hook-form zod@3.21
ディレクトリの構成
-
appディレクトリを使った下図のような構成で作成します。分かりやすくするために、app配下のデフォルトファイルは削除しています。
-
その他の設定等は
npx next-create-app@latest
で作成されたデフォルト設定のままです。/package.json{ "name": "sample-validation", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@hookform/resolvers": "^3.3.2", "@mui/material": "^5.15.0", "next": "14.0.4", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.49.2", "zod": "3.21" }, "devDependencies": { "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", "eslint-config-next": "14.0.4", "typescript": "^5" } }
コード実装手順
ステップ1: 共通のテキストフィールドコンポーネントの作成
まず、@mui/material
のTextField
をラップして、バリデーションとエラーメッセージを表示できる共通のコンポーネントValidationTextField
を作成します。
このコンポーネントはreact-hook-form
のController
を使用し、任意のフォームフィールドに適用できます。
エラーメッセージは、errors
オブジェクトから取得され、ユーザーに適切なフィードバックを提供します。
import { FormControl, TextField, TextFieldProps } from '@mui/material'
import { Control, Controller, FieldErrors, FieldValues, Path } from 'react-hook-form'
// このタイプは、ValidationTextFieldコンポーネントのプロパティを定義します。
type ValdarionTextFieldProps<T extends FieldValues> = {
name: Path<T>
helperText?: string
control: Control<any>
errors: FieldErrors<any>
} & TextFieldProps
export const ValidationTextField = <T extends FieldValues>({
name,
control,
helperText,
errors,
...rest
}: ValdarionTextFieldProps<T>) => {
return (
<Controller
name={name} // フォーム内のフィールド名
control={control} // react-hook-formのcontrolオブジェクト
render={({ field }) => (
<FormControl>
<TextField
name={field.name} // フィールド名
value={field.value} // フィールドの値
onChange={field.onChange} // 値が変更されたときのハンドラ
onBlur={field.onBlur} // フォーカスが外れたときのハンドラ
inputRef={field.ref} // フィールドへの参照
disabled={field.disabled} // 無効化されているかどうか
helperText={helperText} // 追加のヘルパーテキスト
error={!!errors[name]} // エラーがあるかどうか
{...rest} // その他のTextFieldプロパティ
/>
</FormControl>
)}
/>
)
}
型の付け方について
ValidationTextField
コンポーネントでは、ジェネリック型T
を用いて、フォームフィールドの値の型を動的に指定します。これにより、さまざまなフォームデータ構造に柔軟に対応できるようになります。
型ValdarionTextFieldProps<T>
は、必要なプロパティ(name
, control
, errors
)と、オプショナルなhelperText
、さらにTextFieldProps
からの追加プロパティを組み合わせています。
Controller
コンポーネントのrender
プロパティは、実際のテキストフィールドとそのイベントハンドラを定義するために使用されます。
ここでの型指定は、TypeScriptを使用して安全性を高めるとともに、コンポーネントの再利用性を向上させる重要な役割を果たします。
ステップ2: カスタムフックの作成
次に、フォームのロジックを処理するためのカスタムフックuseInputForm
を作成します。
このフックは、React Hook FormとZodを組み合わせて使用し、フォームの状態管理とバリデーションを効率的に行います。
import { zodResolver } from '@hookform/resolvers/zod'
import { DefaultValues, FieldValues, useForm } from 'react-hook-form'
import { ZodSchema, z } from 'zod'
// この型は、useInputFormフックのプロパティを定義します。
type UseInputFormProps<T extends FieldValues> = {
schema: ZodSchema<T> // Zodによるバリデーションスキーマ
defaultValues?: DefaultValues<T> // フォームのデフォルト値
mode?: 'onBlur' | 'onChange' | 'onSubmit' | 'onTouched' | 'all' | undefined // バリデーションモード
}
// useInputFormは、フォームのロジックをカプセル化したカスタムフックです。
export const useInputForm = <T extends FieldValues>({ schema, defaultValues, mode }: UseInputFormProps<T>) => {
// useFormフックを使用して、フォームの状態とイベントハンドラを管理します。
const {
handleSubmit,
control,
formState: { errors },
setError
} = useForm<z.infer<typeof schema>>({
mode, // バリデーションモード
defaultValues, // デフォルト値
resolver: zodResolver(schema) // Zodを使用したバリデーション
})
// カスタムフックは、フォームの状態と関数を返します。
return {
control,
handleSubmit,
formState: { errors },
setError
}
}
フックの構造とロジック
カスタムフックuseInputForm
は、ジェネリック型T
を利用して、様々なフォームデータ構造に適用できるようにしています。
このフックは、以下の主要なプロパティを持つUseInputFormProps
型の引数を受け取ります:
-
schema
: Zodによるバリデーションスキーマ。これにより、入力値のバリデーションルールを定義します。 -
defaultValues
: フォームのデフォルト値を設定します。これはオプショナルです。 -
mode
: バリデーションがトリガーされるタイミングを制御します(例:onBlur
、onChange
)。
useForm
フックを使用して、以下の機能を提供します:
-
handleSubmit
: フォームの送信処理をハンドルする関数。 -
control
: フォームのフィールドをコントロールするためのオブジェクト。 -
errors
: フォームのバリデーションエラーを格納するオブジェクト。 -
setError
: 手動でフォームのエラーを設定する関数。
これらの機能により、フォームのデータ処理とバリデーションロジックを簡潔にし、複数のフォームコンポーネント間で簡単に共有できます。
Zodとの統合
このフックでは、zodResolver
を使用してZodスキーマをReact Hook Formに統合します。
zodResolver
は、Zodスキーマを基にしてフォームデータのバリデーションを行い、エラーメッセージを提供します。
これにより、フォームのバリデーションロジックを別の場所で定義し、カスタムフック内で簡単に適用することができます。
このカスタムフックは、ZodとReact Hook Formの組み合わせにより、フォームのバリデーションロジックを簡潔に保ちながら、フォームの状態管理とバリデーションを効率的に扱うことができます。
ステップ3: バリデーションスキーマの定義
バリデーションのために、zod
を使用してメールアドレスのバリデーションスキーマを定義します。
import { z } from 'zod'
// 正規表現を使用して、有効な文字だけを含む文字列を定義します。
const pattern = /^[\u0021-\u007e]+$/u
// emailMatchSchemaは、メールアドレスのバリデーションルールを定義するオブジェクトです。
export const emailMatchSchema = {
schema: z.object({
email: z
.string()
.nonempty('メールアドレスが入力されていません。') //入力必須バリデーション
.email('正しいメールアドレスを入力してください。') // メールアドレス形式のバリデーション
.regex(pattern) // 追加の正規表現によるバリデーション
}),
defaultValues: { email: '' }, // フォームのデフォルト値
names: { email: 'email' } // フォームフィールドの名前
}
スキーマの構造
emailMatchSchema
オブジェクトでは、以下の要素を定義します:
-
schema
: メールアドレスのバリデーションルールを含むZodスキーマ。これは、入力されたメールアドレスが特定の条件を満たしていることを保証します。- 必須フィールドの指定。
- メールアドレス形式の確認。
- 追加の正規表現によるカスタムバリデーションルール。
-
defaultValues
: フォームのデフォルト値を設定。ここでは空のメールアドレスフィールドを初期状態としています。 -
names
: フォーム内で使用されるフィールド名を定義。これにより、フィールド名の一貫性と再利用性を保証します。
正規表現の使用
スキーマ内で正規表現を使用しています。
この正規表現は、メールアドレスに特定の文字セットが含まれていることを確認するために使用されます。
これにより、メールアドレスの形式をさらに厳密に検証することができます。
ステップ4: フォームコンポーネントの作成
カスタムフックとバリデーションスキーマを使用して、メールアドレスの存在をチェックするフォームコンポーネントCheckExistEmail
を作成します。
コンポーネントの構造と機能
CheckExistEmail
コンポーネントでは、以下の機能を実装します:
-
useInputForm
カスタムフックを使用して、フォームの状態とバリデーションロジックを管理します。これには、メールアドレスのバリデーションスキーマ(emailMatchSchema
)が含まれます。 -
ValidationTextField
コンポーネントを使用して、メールアドレスの入力フィールドを表示します。このフィールドは、エラーメッセージとバリデーションの状態をユーザーに提供します。 -
onSubmit
関数で、フォームの送信時のロジックを定義します。ここでは、入力されたメールアドレスがサンプルメールアドレスと一致するかどうかを確認し、一致しない場合はエラーを表示します。
import { Box, Button } from '@mui/material'
import React from 'react'
import { useInputForm } from '@/hooks/UseInputForm/hook'
import { z } from 'zod'
import { emailMatchSchema } from '@/constants/emailMatchSchema'
import { ValidationTextField } from '../ValidationTextField'
// サンプルメールアドレス
const sampleEmail = 'example@example.com'
export const CheckExistEmail = () => {
// emailMatchSchemaからスキーマ、デフォルト値、フィールド名を取得します。
const { schema, defaultValues, names } = emailMatchSchema
// カスタムフックuseInputFormを使用して、フォームの状態管理とバリデーションを行います。
const {
handleSubmit,
control,
formState: { errors },
setError
} = useInputForm({ schema, defaultValues, mode: 'onBlur' })
// エラーメッセージを取得します。
const errorMessage = errors.email?.message as string
// フォームの送信時の処理を定義します。
const onSubmit = (data: z.infer<typeof schema>) => {
// 入力されたメールアドレスがサンプルと一致しない場合、エラーを設定します。
if (data.email !== sampleEmail) {
setError('email', {
type: 'manual',
message: 'メールアドレスが見つかりません'
})
} else {
// 一致する場合は、通常の処理(例:データベース確認、通知など)を行います。
alert("メールアドレスが一致しました!")
}
}
return (
<Box component="form" onSubmit={handleSubmit(onSubmit)} display="flex" flexDirection="column" gap={3}>
<ValidationTextField
label="メールアドレス"
name={names.email}
control={control}
errors={errors}
helperText={errorMessage}
placeholder="メールアドレスを入力してください"
/>
<Button type="submit" variant="contained">Submit</Button>
</Box>
)
}
フォームの送信処理
フォームの送信処理では、handleSubmit
関数(useInputForm
から提供される)を使用して、送信イベントをハンドルします。
onSubmit
関数では、ValidationTextField
を使用してユーザーにメールアドレス入力を求め、入力されたメールアドレスが特定の条件(この例ではサンプルのメールアドレスとの一致)を満たすかどうかを検証します。
一致しない場合は、setError
関数を使用してカスタムエラーメッセージを設定します。
ステップ5: ページの作成
最後に、作成したフォームコンポーネントを含むページCheckExistEmailPage
を作成します。
appディレクトリ配下のファイルは、配置されたディレクトリをパスとしてルーティングされます。
今回はappディレクトリ直下になるので、ルートパスで表示されるページとなります。
ページ内でCheckExistEmail
を呼び出すだけのコンポーネントです。
'use client'
import { CheckExistEmail } from '@/components/CheckExistEmail'
import React from 'react'
// CheckExistEmailPageは、メールアドレスチェックのための専用ページを定義するコンポーネントです。
const CheckExistEmailPage = () => {
// CheckExistEmailコンポーネントをレンダリングします。
// このコンポーネントは、ユーザーがメールアドレスを入力し、バリデーションを行うためのインターフェースを提供します。
return <CheckExistEmail />
}
export default CheckExistEmailPage
ページとなるコンポーネントは、それ自体DOM構造の定義やロジックを含まず、ルーティングと呼び出すコンポーネントのレンダリングのみに責務を持たせる方が見通しよいと思います。
まとめ
この記事では、react-hook-form
、zod
、そして@mui/material
を組み合わせて、効率的かつ機能的なバリデーションフォームを作成する方法を解説しました。
極力ステップごとに分けて、分かりやすくなるよう心掛けたつもりです。
useStateやuseEffectを使った実装は重くなりがちですが、ライブラリを使うとこんなに簡単に実装できて素晴らしいと思います。(まだ理解が浅いため型付けに苦労しましたが、、、)
割と使いやすく、再利用性のあるコンポーネントとフックにできていると思いますので、ぜひ試してみてください。