はじめに
react
や next.js
でフォーム作成を行う際、react-hook-form
ライブラリは
有力な候補の一つになります。
しかし、例えば名前や趣味の入力欄をそれぞれコンポーネント分割しようと考えた際、
どこに何を定義すればいいのかを最初から判断して実装することはなかなか難しいと思います。
今回は簡単なフォームをコンポーネント分割に注目して実装することで、
react-hook-form
に対する理解を深めていきましょう。
この記事の対象者
フォーム作成のために react-hook-form
を使いたいと思いつつ、
コンポーネント分割の仕方に悩んでいる方。
next.js
でプロジェクトを作成して、簡単なフォーム作成を行います。
また、バリデーションチェックに zod
ライブラリを導入しています。
前提
このあと利用する、各々のバージョン情報を記載しておきます。
library | version |
---|---|
node | 18.17.1 |
npm | 9.6.7 |
next.js | 14.0.1 |
react-hook-form | 7.48.1 |
zod | 3.22.4 |
@hookform/resolvers | 3.3.2 |
今回のプロジェクト構成
完成時、以下の構成になります。
※主要ファイルのみを記載
※折りたたみを開いてご確認ください
ディレクトリ構成
root
└─src
├─app
│ globals.css
│ layout.tsx
│ page.tsx
│
└─components
├─app
│ │ index.ts
│ │
│ ├─Hobby
│ │ formSchema.ts
│ │ index.tsx
│ │
│ └─Name
│ formSchema.ts
│ index.tsx
│
└─parts
│ index.ts
│
├─Button
│ index.tsx
│
└─InputText
index.tsx
プロジェクトの作成&下準備
それでは、プロジェクトを作成して必要なライブラリをインストールしましょう。
プロジェクト作成
ローカルの任意のディレクトリで以下コマンドを実行します。
npx create-next-app@latest
いくつか質問されますが、すべてデフォルトで大丈夫です。
(フォルダ名は自分の好きな名前に変えておきましょう)
What is your project named? my-app
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
必要なライブラリのインストール
今回の主役である react-hook-form
をインストールします。
このライブラリにより、フォームの値管理がとても楽になります。
今回詳しい説明はしないので、興味のある方は 公式サイト の
Get Started タブの箇所だけでもざっと確認してみてください。
npm install react-hook-form
次に、バリデーションチェックを楽にしてくれる zod
をインストールします。
詳しく知りたい方は 公式ドキュメント をご確認ください。
npm install zod
最後に、react-hook-form
で zod
を利用するために @hookform/resolvers
をインストールします。
詳しく知りたい方は npm ドキュメント を (ry。
npm install @hookform/resolvers
これにて、必要なライブラリをすべてインストールすることが出来ました。
設定ファイルとcssファイルに変更を加える
このままではいくつかエラーが出てしまうので、設定ファイルに修正を加えます。
また、不要なcssを削除します。
.eslintrc.json
ESLintのデフォルト設定では関数に名前を付けない場合エラーを吐くので、
その機能をオフにします。
{
- "extends": "next/core-web-vitals"
+ "extends": "next/core-web-vitals",
+ "rules": {
+ "react/display-name": 0
+ }
}
tsconfig.json
moduleResolution関係でエラーが出る場合は以下修正を加えてください。
Qiitaにこのエラー内容を投稿しているので、気になった方はぜひご確認ください。
{
"compilerOptions": {
・・・
- "moduleResolution": "bundler",
+ "moduleResolution": "Node16",
・・・
},
・・・
}
globals.css
不要なcssを削除します。
上3行は必要なので残しておいてください。
@tailwind base;
@tailwind components;
@tailwind utilities;
- :root {
- --foreground-rgb: 0, 0, 0;
- --background-start-rgb: 214, 219, 220;
- --background-end-rgb: 255, 255, 255;
- }
-
- @media (prefers-color-scheme: dark) {
- :root {
- --foreground-rgb: 255, 255, 255;
- --background-start-rgb: 0, 0, 0;
- --background-end-rgb: 0, 0, 0;
- }
- }
-
- body {
- color: rgb(var(--foreground-rgb));
- background: linear-gradient(
- to bottom,
- transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb));
- }
.prettierrc (任意)
.prettierrcを設定しているので参考にどうぞ。
(ソースコードを整形する機能なので、なくても動作します)
{
"printWidth": 120,
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"endOfLine": "lf"
}
その他ファイル(任意)
プロジェクト生成時に自動で作成された、以下のファイルは削除してしまって大丈夫です。
- [root]\public\next.svg
- [root]\public\vercel.svg
- [root]\src\app\favicon.ico
また、layout.tsx
の metadata
は、今回用に文言を変更しておきましょう。
export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
+ title: 'react-hook-form',
+ description: "Let's separate components while using react-hook-form!",
}
以上で下準備はOKです。
基礎となる部品を作成する
まずは基本パーツを作っていきます。
今回はButtonコンポーネントと、ユーザがテキストを入力するための
InputTextコンポーネントを作成します。
Button
src\components\parts\Button
ディレクトリの下にコンポーネントを作成します。
このコンポーネントは、button
タグが持つデフォルトの属性をプロパティに持ち、
また children
には String
型のみを受け付けます。
サンプルコード
import React, { forwardRef } from 'react';
type BaseProps = {
children?: string;
};
type Props = BaseProps & Omit<React.ComponentPropsWithoutRef<'button'>, keyof BaseProps | 'className' | 'style'>;
export const Button = forwardRef<HTMLButtonElement, Props>(
({ children = 'ボタン', type = 'button', ...buttonAttributes }, ref) => {
return (
<button
ref={ref}
type={type}
className='flex justify-center items-center w-full h-12 rounded-3xl
text-base text-white disabled:text-gray-100
bg-blue-500 disabled:bg-gray-300
duration-300
enabled:hover:opacity-50'
{...buttonAttributes}
>
{children}
</button>
);
}
);
InputText
src\components\parts\InputText
ディレクトリの下にコンポーネントを作成します。
このコンポーネントは、input
タグが持つデフォルトの属性と、
エラーメッセージ用の errMsg
、ラベルテキスト用の label
を プロパティに持ちます。
サンプルコード
import { forwardRef } from 'react';
type BaseProps = {
label: string;
errMsg?: string;
};
type Props = BaseProps & Omit<React.ComponentPropsWithoutRef<'input'>, keyof BaseProps | 'className' | 'style'>;
export const InputText = forwardRef<HTMLInputElement, Props>(({ label, errMsg, ...inputAttributes }, ref) => {
return (
<div className='flex flex-col gap-1'>
<label htmlFor={label}>{label}</label>
<div>
{errMsg && <p className='text-sm text-red-500'>{errMsg}</p>}
<input
type='text'
ref={ref}
id={label}
className='flex w-full p-3 rounded bg-white
border border-solid border-gray-200
focus:outline-none'
{...inputAttributes}
/>
</div>
</div>
);
});
インデックスファイル
ButtonコンポーネントとInputTextコンポーネントを取得するための
インデックスファイルを作成します。
サンプルコード
import { Button } from './Button';
import { InputText } from './InputText';
export { Button, InputText };
コンポーネント分割を意識して画面を作成する
いよいよ react-hook-form
に触れていきます。
page.tsx
に画面の中身を実装しますが、名前の入力フォームや趣味の入力フォームは
コンポーネント分割して別ファイルに実装していきます。
page.tsxを作成する
まずは page.tsx
の実装を行います。
@/components/app/index
からインポートしている Hobby
と Name
は後ほど実装します。
ここで注目したいのは、
FormProvider
でフォームを囲うことで、コンポーネント分割した Hobby
や Name
内でも
useForm
から取得した共通の methods
を使えるようになる
ということです。
サンプルコード
'use client';
import { Hobby, Name } from '@/components/app/index';
import { Button } from '@/components/parts';
import { FormProvider, useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback } from 'react';
// フォームに入力される値すべての型とバリデーションチェックを定義するフォームスキーマ
const formSchema = z.object({
name: Name.formSchema,
hobby: Hobby.formSchema,
});
// フォームスキーマから型定義のみを抽出
type Form = z.infer<typeof formSchema>;
const Page = () => {
const methods = useForm<Form>({
mode: 'onChange',
// resolverに上で定義したformSchemaをセットすることで、
// zodで定義したバリデーションチェックが機能するようになる
resolver: zodResolver(formSchema),
});
// Submitされた場合、フォームの入力値をリセットする
const onSubmit = useCallback(() => {
methods.reset({
name: {
lastName: '',
firstName: '',
},
hobby: { hobby: '' },
});
}, [methods]);
return (
<div className='flex flex-col gap-8 py-10'>
<h1 className='flex justify-center text-2xl font-bold'>{'react-hook-formを学ぼう!'}</h1>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<div className='flex flex-col gap-8 max-w-lg px-5 m-auto'>
<div className='flex flex-col gap-4'>
<Name.Component />
<Hobby.Component />
</div>
<Button type='submit' disabled={!methods.formState.isValid}>
リセット
</Button>
</div>
</form>
</FormProvider>
</div>
);
};
export default Page;
インデックスファイル
あらかじめ、名前入力用フォームと趣味入力用フォームのインデックスファイルを
作成しておきます。
サンプルコード
import * as Name from './Name';
import * as Hobby from './Hobby';
export { Name, Hobby };
名前入力用フォーム
名前入力用のフォームを作成します。
(page.tsx
の Name
に該当)
useFormContext
を使って register
やエラー情報を取得しますが、
返り値の型は名前入力用フォームで使用する型に限定させましょう。
サンプルコード
import { z } from 'zod';
// 名前入力用フォームで入力される値の型とバリデーションチェックを定義するフォームスキーマ
export const formSchema = z.object({
lastName: z.string().min(1, '入力必須です').max(20, '20字以内で入力ください'),
firstName: z.string().min(1, '入力必須です').max(20, '20字以内で入力ください'),
});
// フォームスキーマから型定義のみを抽出
export type Form = z.infer<typeof formSchema>;
import { InputText } from '@/components/parts';
import { useFormContext } from 'react-hook-form';
import { formSchema, Form } from './formSchema';
export { formSchema };
export const Component = () => {
const {
register,
formState: { errors },
} = useFormContext<Record<'name', Form>>();
return (
<div className='flex flex-col gap-4'>
<InputText
label='苗字'
errMsg={errors?.name?.lastName?.message}
placeholder='鈴木'
{...register('name.lastName')}
/>
<InputText
label='名前'
errMsg={errors?.name?.firstName?.message}
placeholder='太郎'
{...register('name.firstName')}
/>
</div>
);
};
趣味入力用フォーム
名前入力用フォームと同じように趣味入力用フォームも作成します。
(page.tsx
の Hobby
に該当)
サンプルコード
import { z } from 'zod';
// 趣味入力用フォームで入力される値の型とバリデーションチェックを定義するフォームスキーマ
export const formSchema = z.object({
hobby: z.string().min(1, '入力必須です').max(50, '50字以内で入力ください'),
});
// フォームスキーマから型定義のみを抽出
export type Form = z.infer<typeof formSchema>;
import { InputText } from '@/components/parts';
import { useFormContext } from 'react-hook-form';
import { formSchema, Form } from './formSchema';
export { formSchema };
export const Component = () => {
const {
register,
formState: { errors },
} = useFormContext<Record<'hobby', Form>>();
return (
<InputText
label='趣味'
errMsg={errors?.hobby?.hobby?.message}
placeholder='野球、サッカー'
{...register('hobby.hobby')}
/>
);
};
完成
上記を実装できたら、ぜひ npm run dev
を実行して動作確認してみてください。
キャプチャのような画面がブラウザで表示され、
簡単なバリデーションチェックが可能なフォームを確認できると思います。
まとめ
まとめとして、今までの内容を超コンパクトにしたものを記載しておきます。
zod等の実装がない分とても見やすいです。
'use client';
import { FormProvider, useForm } from 'react-hook-form';
export type Form = {
// フォームに入力される値において、すべての型を定義
name: string,
hobby: string,
};
const Page = () => {
// フォームの値管理に必要なオブジェクトを取得
const methods = useForm<Form>({
mode: 'onChange',
});
return <FormProvider {...methods}>{/* フォーム実装 */}</FormProvider>;
};
export default Page;
import { Form } from '@/app/page';
import { useFormContext } from 'react-hook-form';
export const Component = () => {
const { register } = useFormContext<Form>();
return (
<input
className='border'
{...register('name', {
// バリデーションチェック実装 (zodを利用しない場合はここに記述する)
required: {
value: true,
message: '入力が必須の項目です。',
},
})}
/>
);
};
締め
react-hook-form
を利用したフォームのコンポーネント分割について、
理解を深めることはできたでしょうか?
この記事がだれかのお役に立てたら幸いです。