Michi's Tech Blog

一人前のWebエンジニアを目指して

React Hook Formでコンポーネントを分割する方法

こんにちは!
スマレジ テックファームのMichiです!

今回はタイトルの通り、React Hook Formでコンポーネントを分割する方法をご紹介します。

解説

サンプル

サンプルとして、 メールアドレス、ユーザー名、パスワードを入力するシンプルな登録フォームを用意しました。それぞれのフィールドには、React Hook Form で必須入力のバリデーションをかけています。

SignUp.tsx

import { useForm } from "react-hook-form";

type FormData = {
  email: string;
  username: string;
  password: string;
};

export const SignUp = () => {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<FormData>();
  const onSubmit = (data: FormData) => console.log(data);

  return (
    <>
      <h1>登録フォーム</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="field">
          <label htmlFor={"email"}>メールアドレス</label>
          <input id="email" {...register("email", { required: true })} />
          {errors.email && (
            <span className="error">このフィールドは必須です</span>
          )}
        </div>

        <div className="field">
          <label htmlFor={"username"}>ユーザー名</label>
          <input id="username" {...register("username", { required: true })} />
          {errors.username && (
            <span className="error">このフィールドは必須です</span>
          )}
        </div>

        <div className="field">
          <label htmlFor={"password"}>パスワード</label>
          <input
            id="password"
            {...register("password", { required: true })}
            type="password"
          />
          {errors.password && (
            <span className="error">このフィールドは必須です</span>
          )}
        </div>

        <button className="button" type="submit">
          登録
        </button>
      </form>
    </>
  );
};

これら3つのフィールドは同じ作りなので、別コンポーネントに切り出して抽象化したいです。

抽象化

まず、SignUp.tsxを次のように修正します。

SignUp.tsx

import { FormProvider, useForm } from "react-hook-form";
import { InputField } from "./InputField";

export type FormData = {
  email: string;
  username: string;
  password: string;
};

export const SignUp = () => {
  const methods = useForm<FormData>();
  const onSubmit = (data: FormData) => console.log(data);

  return (
    <>
      <h1>登録フォーム</h1>
      <FormProvider {...methods}>
        <form onSubmit={methods.handleSubmit(onSubmit)}>
          <InputField name="email" labelText="メールアドレス" />
          <InputField name="username" labelText="ユーザー名" />
          <InputField name="password" labelText="パスワード" type="password" />
          <button className="button" type="submit">
            登録
          </button>
        </form>
      </FormProvider>
    </>
  );
};

InputFieldは各フィールドを抽象化したコンポーネントで、次に説明します。

ここでのポイントは、FormProviderでフォーム全体をラップすることです。propsには、useFormから取り出したmethodsを丸ごと渡します。

こうすることで、各InputFieldへ個別にpropsを渡さなくても、それぞれのコンポーネントmethodsを利用できるようになります。

次に、InputField.tsxの実装を確認します。

InputField.tsx

import { useFormContext } from "react-hook-form";

type InputFieldProps = {
  name: string;
  labelText: string;
} & JSX.IntrinsicElements["input"];

export const InputField = ({ name, labelText, ...others }: InputFieldProps) => {
  const {
    register,
    formState: { errors }
  } = useFormContext();

  return (
    <div className="field">
      <label htmlFor={name}>{labelText}</label>
      <input id={name} {...register(name, { required: true })} {...others} />
      {errors[name] && <span className="error">このフィールドは必須です</span>}
    </div>
  );
};

先ほど、親コンポーネントからFormProvidermethodsを受け取っているので、 組み込み関数のuseFormContextを呼び出すだけで、methodsの各プロパティが利用できます。

あとは、<label><input>要素に適用していくだけです。

propsのothersには、JSX.IntrinsicElements["input"]各属性が入るので、親から属性値を渡すことで<input>フォームをカスタマイズできます。

おわりに

React Hook Formでコンポーネントを分割する方法をご紹介しました。

React Hook Formは便利なライブラリですが、学習コストがなかなか高いので、しっかり使いこなすには勉強が必要だなと思いました。