Next.jsのバリデーションでreact-hook-formを利用した話
Next.js を触っている時に、メール送信フォームを作成していました。
私はバックエンドの人間なので、Next.js のバリデーション周りに関してどうすればいいのか悩んでいました。
どうやら react-hook-form
というものがあるらしく、今回それを用いてフォームバリデーションを実装していきます。
使ってみたら、とても簡単でした。
react-hook-form のインストール
yarn or npm でライブラリをインストールします。
yarn add react-hook-form
or
npm install react-hook-form
使い方
大体のことは下記ページを見ると実装できちゃいます。
つまり、useForm を利用し、submit 時に handleSubmit を呼び出し、引数に実行したい関数を指定します。
register()について
ちょっとばかり奇妙な構文が..。
<input defaultValue="test" {...register("example")} />
<input {...register("exampleRequired", { required: true })} />
この register は一体何をしているのでしょうか。
まずは、公式ドキュメントを見る。
そのあとは、 node_modules/react-hook-form/dist/types/form.d.ts
の中を見ます。
/**
* Register field into hook form with or without the actual DOM ref. You can invoke register anywhere in the component including at `useEffect`.
*
* @remarks
* [API](https://react-hook-form.com/api/useform/register) • [Demo](https://codesandbox.io/s/react-hook-form-register-ts-ip2j3) • [Video](https://www.youtube.com/watch?v=JFIpCoajYkA)
*
* @param name - the path name to the form field value, name is required and unique
* @param options - register options include validation, disabled, unregister, value as and dependent validation
*
* @returns onChange, onBlur, name, ref, and native contribute attribute if browser validation is enabled.
*
* @example
* ```tsx
* // Register HTML native input
* <input {...register("input")} />
* <select {...register("select")} />
*
* // Register options
* <textarea {...register("textarea", { required: "This is required.", maxLength: 20 })} />
* <input type="number" {...register("name2", { valueAsNumber: true })} />
* <input {...register("name3", { deps: ["name2"] })} />
*
* // Register custom field at useEffect
* useEffect(() => {
* register("name4");
* register("name5", { value: '"hiddenValue" });
* }, [register])
*
* // Register without ref
* const { onChange, onBlur, name } = register("name6")
* <input onChange={onChange} onBlur={onBlur} name={name} />
* ```
*/
export declare type UseFormRegister<TFieldValues extends FieldValues> = <
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>(
name: TFieldName,
options?: RegisterOptions<TFieldValues, TFieldName>
) => UseFormRegisterReturn;
つまり、register 関数では name と RegisterOptions を引数にとって、submit 時に JSON 形式で値を取得できるようです。
そして、useEffect にも組み込むことができるよう。
公式では、下記2つのコードは同じ意味であると書かれています。
const { onChange, onBlur, name, ref } = register("firstName");
// include type check against field path with the name you have supplied.
<input
onChange={onChange} // assign onChange event
onBlur={onBlur} // assign onBlur event
name={name} // assign name prop
ref={ref} // assign ref prop
/>;
<input {...register("firstName")} />
RegisterOptions ですが、下記のようなバリデーションを準備してくれています。
pattern があって正規表現も対応しているので、大体のフォームには対応できるかと。
- required
- maxLength
- minLength
- max
- min
- pattern
- validate
- valueAsNumber
- valueAsDate
- setValueAs
- disabled
- onChange
- onBlur
- value
- shouldUnregister
- deps
watch
watch は、各入力フォームの監視が可能です。
例えば、useEffect に watch を監視させておけば、入力フォームに変更がある際に、レンダリングを走らせることが可能となります。
formState
フォーム状態について全体に関する情報が含まれているようです。
ただ、利用する際は事前に展開しておく必要があるようです。
// ❌ formState.isValid is accessed conditionally,
// so the Proxy does not subscribe to changes of that state
return <button disabled={!formState.isDirty || !formState.isValid} />;
// ✅ read all formState values to subscribe to changes
const { isDirty, isValid } = formState;
return <button disabled={!isDirty || !isValid} />;
handleSubmit
フォーム入力値の検証が問題なく成功したら、この関数でフォームデータを受け取るため、コールバック関数となっています。
一方、エラーが発生した時のコールバック関数も用意されているようです。
// 成功時
SubmitHandler (data: Object, e?: Event) => void
// 失敗時
SubmitErrorHandler (errors: Object, e?: Event) => void
コンポーネント化における問題点の解決
しかし、上記の実装となるとコンポーネント化が難しくなります。
その問題を解決するため、2 つの方法があります。
- 親コンポーネントから子に register 関数を渡して登録する方法
- フォーム要素に ref を与えて管理する方法
親コンポーネントから子に register 関数を渡して登録する方法
親ファイル。
import React from "react";
import { useForm, SubmitHandler } from "react-hook-form";
import { Input } from "./Input";
import { Select } from "./Select";
import "./styles.css";
export interface IFormValues {
"First Name": string;
Age: number;
}
function App() {
const { register, handleSubmit } = useForm<IFormValues>();
const onSubmit: SubmitHandler<IFormValues> = (data) => {
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input label="First Name" register={register} required />
<Select label="Age" {...register("Age")} />
<input type="submit" />
</form>
);
}
export default App;
Input コンポーネントに props として、 label
、register
、required
を渡しています。
<Input label="First Name" register={register} required />
子ファイル。
Input.tsx
import { Path, UseFormRegister } from "react-hook-form";
import { IFormValues } from "./App";
type InputProps = {
label: Path<IFormValues>;
register: UseFormRegister<IFormValues>;
required: boolean;
};
// <input>要素を含んだ子コンポーネント
export const Input = ({ label, register, required }: InputProps) => (
<label>
{label}
<input {...register(label, { required })} />
</label>
);
フォーム要素に ref を与えて管理する方法があります。
ただし、React の公式ドキュメントにも記載されているのですが、ref を子に渡すことができず、利用もできません。
通常の関数またはクラスコンポーネントは ref 引数を受け取らず、ref は props からも利用できません。
親が与えた ref
を受け取るために用いるのが React.forwardRef
です。
子ファイル。
Select.tsx
import React from "react";
import { UseFormRegister } from "react-hook-form";
import { IFormValues } from "./App";
// React.forwardRefを利用し、refを渡す
export const Select = React.forwardRef<
HTMLSelectElement,
{ label: string } & ReturnType<UseFormRegister<IFormValues>>
>(({ onChange, onBlur, name, label }, ref) => (
<label>
{label}
<select name={name} ref={ref} onChange={onChange} onBlur={onBlur}>
<option value="20">20</option>
<option value="30">30</option>
</select>
</label>
));
ref
を使うことで Select.tsx の DOM がselectタグ
であることを認識できるようになります。
register 関数には、onChange
、onBlur
、ref
、name
プロパティを返します。
なので、親から{...register("Age")}
と指定することで、name や ref などが子で利用できるということです。
※そもそも、React における ref というのは、ちょっと難しいところなので別途解説する予定です。
まとめ
react-hook-form は汎用性が高く、既存の組み込まれた UI コンポーネントに対しても、useController を使うことで比較的簡単に導入できます。
フロント側で最低限のバリデーションを実装し、UX 向上を図るのはとても大切なことなので、積極的に導入できたらと思いました。
今回参考にした記事
(下記はパターンで細かく分けられていて、とても勉強になりました)
Discussion
registerを引数に渡さずにuseFormContextで持ち回るようにしてデモを作ってみました。
簡単ですが、以上です。