
はじめに
前回はReact Hook FormのuseFormとZodを組み合わせて入力フォームを作成しました。
各ファイルに記述する分にはuseFormのregisterを使用すれば問題無く実装できます。
ただ、入力フォームは色々なところで使用することが多いと思いますので、できたら共通化したいですよね。
そこで今回はReact Hook FormのuseControllerも組み合わせて、テキストボックスやセレクトボックスの入力欄を共通化してみたいと思います。
registerやZodについては以前紹介した記事をご参照ください!
useControllerとは?
再利用可能な入力フォームを作成するためのカスタムフック
基本的にはuseFormで定義した項目名やcontrolを受け取り、useControllerのonChangeメソッドを使用してuseFormの値を更新・バリデーションの実行を行います。
useFormのControllerと同じような感覚ですが、大きく違う点は「nameやcontrolの指定方法」です。
- Controller:Controllerタグの中でnameやcontrolを指定
- useController:UIとは別にuseControllerの引数に指定
簡単に言えば、UIと制御を分けて記載することが可能です。
// Controllerの場合は要素の中に指定
<Controller
control={ control }
name={ name }
render={ 省略 }
/>
// useControllerの場合は引数に指定
const { 省略 } = useController<T>({ name, control });
作ってみよう
以下の条件で入力フォームを作成してみます。
| 項目名 | 要素 | 入力制限 |
| 氏名 | テキストボックス | 必須 100文字以内 |
| フリガナ | テキストボックス | 必須 カタカナ 100文字以内 |
| 性別 | セレクトボックス | 必須 |
| メールアドレス | テキストボックス | 必須 254文字以内 メールアドレス形式 |
| 郵便番号 | テキストボックス | 必須 8文字以内 {半角数字3桁}-{半角数字4桁} |
| 住所 | テキストボックス | 必須 100文字以内 |
| 建物名 | テキストボックス | 100文字以内 |
| 職業 | セレクトボックス | 必須 |
| 交通手段 | セレクトボックス | 必須 |
| 趣味 | セレクトボックス | 必須 |
完成系
テキストエリアのコンポーネント
import { useController, type UseControllerProps, type FieldValues } from 'react-hook-form';
type Props<T extends FieldValues> = UseControllerProps<T>;
const InputForm = <T extends FieldValues>(props: Props<T>) => {
const { name, control } = props;
const {
field: { ref, onChange },
fieldState: { error },
} = useController<T>({ name, control });
return (
<div>
<input type="text" ref={ref} onChange={(e) => onChange(e.target.value)} />
{error?.message && <span style={{ color: 'red' }}>{error.message}</span>}
</div>
);
};
export default InputForm;
セレクトボックスのコンポーネント
import { useController, type FieldValues, type UseControllerProps } from 'react-hook-form';
type SelectBoxOption = {
label: string;
value: string;
};
type Props<T extends FieldValues> = {
options: SelectBoxOption[];
} & UseControllerProps<T>;
const SelectBoxForm = <T extends FieldValues>(props: Props<T>) => {
const { name, control, options } = props;
const {
field: { ref, onChange },
fieldState: { error },
} = useController<T>({ name, control });
return (
<div>
<select ref={ref} onChange={onChange}>
<option key="0" value="" />
{options.map((data) => {
return (
<option key={data.value} value={data.value}>
{data.label}
</option>
);
})}
</select>
{error?.message && <span style={{ color: 'red' }}>{error.message}</span>}
</div>
);
};
export default SelectBoxForm;
親ファイル
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import InputForm from '$/components/atom/InputForm';
import SelectBoxForm from '$/components/atom/SelectBoxForm';
const SexOptions = [{ value: '1', label: '男性' }, { value: '2', label: '女性' }];
const OccupationOptions = [{ value: '1', label: '学生' }, { value: '2', label: '社会人' }];
const TransportOptions = [{ value: '1', label: '徒歩' }, { value: '2', label: '電車' }];
const HobbyOptions = [{ value: '1', label: '運動' }, { value: '2', label: 'ゲーム' }];
// Zodでのスキーマ設定
const Schema = z.object({
name: z.string().nonempty({ message: '氏名を入力してください。' }).max(100),
kana: z
.string()
.nonempty({ message: 'フリガナを入力してください。' })
.regex(/^[ァ-ンヴー]+$/, {
message: 'カタカナで入力してください。',
})
.max(100),
sex: z.string().nonempty({ message: '性別を選択してください。' }),
mail: z
.string()
.nonempty({ message: 'メールアドレスを入力してください。' })
.max(254)
.email({ message: 'メールアドレス形式で入力してください。' }),
code: z
.string()
.nonempty({ message: '郵便番号を入力してください。' })
.regex(/^[\d]{3}-[\d]{4}$/, {
message: '郵便番号形式で入力してください。',
})
.max(8),
address: z.string().nonempty({ message: '住所を入力してください。' }).max(100),
building: z.string().max(100),
occupation: z.string().nonempty({ message: '職種を選択してください。' }),
transport: z.string().nonempty({ message: '交通手段を選択してください。' }),
hobby: z.string().nonempty({ message: '趣味を選択してください。' }),
});
const UseControllerNormal = () => {
const { control, handleSubmit } = useForm({
defaultValues: {
name: '',
kana: '',
sex: '',
mail: '',
code: '',
address: '',
building: '',
occupation: '',
transport: '',
hobby: '',
},
mode: 'onChange',
resolver: zodResolver(Schema),
});
const onSubmit = handleSubmit(() => {
alert('成功');
});
return (
<div>
<div>
<label>氏名</label>
<InputForm control={control} name="name" />
</div>
<div>
<label>フリガナ</label>
<InputForm control={control} name="kana" />
</div>
<div>
<label>性別</label>
<SelectBoxForm control={control} name="sex" options={SexOptions} />
</div>
<div>
<label>メールアドレス</label>
<InputForm control={control} name="mail" />
</div>
<div>
<label>郵便番号</label>
<InputForm control={control} name="code" />
</div>
<div>
<label>住所</label>
<InputForm control={control} name="address" />
</div>
<div>
<label>建物名</label>
<InputForm control={control} name="building" />
</div>
<div>
<label>職業</label>
<SelectBoxForm control={control} name="occupation" options={OccupationOptions} />
</div>
<div>
<label>交通手段</label>
<SelectBoxForm control={control} name="transport" options={TransportOptions} />
</div>
<div>
<label>趣味</label>
<SelectBoxForm control={control} name="hobby" options={HobbyOptions} />
</div>
<button onClick={onSubmit}>確定</button>
</div>
);
};
export default UseControllerNormal;
解説
今回はuseControllerの機能を中心に解説していきます。
useFormと組み合わせているので、使用していないプロパティ等が出てきますが割愛させていただきます。
zodやuseFormについては前回の記事で解説していますので、そちらをご参照ください。
Props
親コンポーネントからuseFormの型を参照するようにするためUseControllerPropsを使用します。
共通化ということで、親から型を定義するために抽象的な型引数<T>を与えています。
import type { UseControllerProps, FieldValues } from 'react-hook-form';
export type Props<T extends FieldValues> = UseControllerProps<T>;
useControllerのメソッドについて
今回はuseForm + Zodと組み合わせているので、基本的なメソッドのみを使用しています。
useController単体で実装する際には、defaultValue(初期値)やrules(バリデーション)などを設定します。
useControllerの引数にはPropsで受け取ったnameとcontrolを指定します。
const {
field: { ref, onChange },
fieldState: { error },
} = useController<T>({ name, control });
- onChange:useFormで定義した値を更新する
- error:Zodで定義したバリデーションを実行した結果
最後に
冒頭でもお話ししましたが、やはり共通化には適してると思いました。
親から項目名やバリデーションルールを渡してあげるだけで、onChangeの制御やエラー表示などを行ってくれます。
そのため、レイアウトやonChange時の処理などを1ファイルで一元管理することができます。
レイアウトや動作に統一感を持たせることができるので個人的には好印象でした。
最低限、nameとcontrolを渡すだけで作れるというのも初心者にもわかりやすいと思います。
もしテキストフィールドなどを共通化したいという課題があれば検討してみてください。
公式サイトのリンクも載せておきますので、ぜひぜひご参照ください!
https://react-hook-form.com/docs/usecontroller
