🌊

Next.jsのバリデーションでreact-hook-formを利用した話

2022/05/04に公開約6,700字

Next.js を触っている時に、メール送信フォームを作成していました。

私はバックエンドの人間なので、Next.js のバリデーション周りに関してどうすればいいのか悩んでいました。

どうやら react-hook-form というものがあるらしく、今回それを用いてフォームバリデーションを実装していきます。

使ってみたら、とても簡単でした。

react-hook-form のインストール

yarn or npm でライブラリをインストールします。

yarn add react-hook-form

or

npm install react-hook-form

使い方

大体のことは下記ページを見ると実装できちゃいます。

https://react-hook-form.com/jp/get-started/#Quickstart

つまり、useForm を利用し、submit 時に handleSubmit を呼び出し、引数に実行したい関数を指定します。

register()について

ちょっとばかり奇妙な構文が..。

<input defaultValue="test" {...register("example")} />

<input {...register("exampleRequired", { required: true })} />

この register は一体何をしているのでしょうか。

まずは、公式ドキュメントを見る。

https://react-hook-form.com/api/useform/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 を監視させておけば、入力フォームに変更がある際に、レンダリングを走らせることが可能となります。

https://react-hook-form.com/api/useform/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 つの方法があります。

  1. 親コンポーネントから子に register 関数を渡して登録する方法
  2. フォーム要素に 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 として、 labelregisterrequiredを渡しています。

<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 関数には、onChangeonBlurrefnameプロパティを返します。

なので、親から{...register("Age")}と指定することで、name や ref などが子で利用できるということです。

※そもそも、React における ref というのは、ちょっと難しいところなので別途解説する予定です。

まとめ

react-hook-form は汎用性が高く、既存の組み込まれた UI コンポーネントに対しても、useController を使うことで比較的簡単に導入できます。

フロント側で最低限のバリデーションを実装し、UX 向上を図るのはとても大切なことなので、積極的に導入できたらと思いました。

今回参考にした記事

https://react-hook-form.com/jp/get-started

https://react-hook-form.com/api

(下記はパターンで細かく分けられていて、とても勉強になりました)

https://qiita.com/FumioNonaka/items/943909dee793ee63416b
GitHubで編集を提案

Discussion

ログインするとコメントできます