React Hook Form を使うと、 state を書く手間が省けられ、フォーム入力を簡単に扱えます。

公式ガイド にも書かれているように、使用するに必要な props を簡単に渡せます:

<input {...register("name")} />

自作入力コンポーネントを作り、追加機能を持たせたいときもあって:

// label と <input /> の既存 props を持たせる
const LabeledInput = ({ label, ...props }) => (
  <label>
    {label}
    <input {...props} />
  </label>
);

...

<LabeledInput label="Name" placeholder="Enter your name..." required />

標準の <input /> と同じように register しようとすると:

<LabeledInput label="Name" {...register} />

入力は問題なく反映されましたが、フォームを送信した際に実際の入力値が取得できませんでした。

これを解決するためには、まず {...register} が何をしているかを理解することが大切です。公式ドキュメント を見てみると、実際にはこれらの props を渡しています:

const { onChange, onBlur, name, ref } = register('name');

...

<input onChange={onChange} onBlur={onBlur} name={name} ref={ref} />

最初の 3 つは props として渡すのは簡単ですが、最後の ref を渡すのが少し厄介です。この ref がないとフォーム送信時に各フィールドの値は得られません。

この問題に対して 1 つの解決策として、別の prop として ref を渡すこと:

const registerWithRef = (name) => {
  const { ref, ...rest } = register(name);
  return { ...rest, innerRef: ref };
};

...

<LabeledInput label="First Name" {...registerWithRef("firstName")} />

しかし、より簡単な方法があります。それは forwardRef を活用することです:

import { forwardRef } from 'react';

const LabeledInput = forwardRef(({ label, ...props }, ref) => (
  <label>
    {label}
    <input ref={ref} {...props} />
  </label>
));

// デバッグメッセージ用に表示名を追加し、eslint エラーを避ける
LabeledInput.displayName = "LabeledInput";

...

// 標準の <input /> と同じように動作します
<LabeledInput label="First Name" {...register("firstName")} />

これで標準の入力と同じように動作する自作入力コンポーネントが作れました。

別のアプローチ 見出しへのリンク

もう一つのアプローチは、 registername をコンポーネントに渡し、コンポーネント内で register する方法です。さらに、エラーメッセージも扱えるように、 errors を渡すこともできます。

import { ErrorMessage } from "@hookform/error-message"

const LabeledInput = ({ name, register, label, errors, ...props }) => (
  <label>
    {label}
    <input {...register(name)} {...props} />
    <ErrorMessage errors={errors} name={name} />
  </label>
);

...

<LabeledInput label="First Name" name="firstName" register={register} error={errors} />