7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Hook Form 入門 Part 3 — Controller と useFieldArray を実務で使いこなす

7
Posted at

React Hook Form 入門 Part 3 — Controller と useFieldArray を実務で使いこなす

読了時間の目安: 約15分 · React Hook Form v7 対応

シリーズ構成

  • Part 1: RHF の仕組みを理解してから使う
  • Part 2: バリデーション設計を実務レベルに引き上げる
  • Part 3: Controller と useFieldArray(本記事)

Part 1 では RHF がなぜ速いのかを整理し、Part 2 ではバリデーション設計を掘り下げました。今回は多くの人が最初につまずく問題を解決します。

「なぜ register だけでは動かないのか?」

特に以下のようなケースで起きます。

  • UI ライブラリ(MUI / Ant Design / Chakra UI)の入力コンポーネント
  • DatePicker / Select / React Select
  • 動的に増減するフォーム(電話番号、住所、メンバー追加)

ここで登場するのが ControlleruseFieldArray です。この 2 つを理解すると、React Hook Form を実務でかなり自由に扱えるようになります。


なぜ register では動かないのか

Part 1 で説明した通り、RHF は uncontrolled component を前提に設計されています。

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

これは普通の HTML input なので問題ありません。ref を渡せて、値を DOM が持ち、RHF が直接参照できるからです。

しかし UI ライブラリのコンポーネントは多くの場合 controlled component として作られています。

<DatePicker />
<Select />
<ReactSelect />

これらは valueonChange で管理されていて、ref ベースではありません。そのため {...register()} では正しく動きません。ここで使うのが Controller です。


Controller とは何か

Controller は controlled component を RHF に接続するための橋 です。

UI ライブラリのコンポーネント
        ↓
    Controller
        ↓
  React Hook Form

register が使えない場所では、ほぼ Controller を使います。

基本的な使い方

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

const { control, handleSubmit } = useForm();

<Controller
  name="username"
  control={control}
  render={({ field }) => (
    <Input {...field} />
  )}
/>

field の正体

render={({ field }) => ...}field には以下が入っています。

{
  value,
  onChange,
  onBlur,
  name,
  ref
}

つまり <Input {...field} /> は実質的に以下と同じです。

<Input
  value={value}
  onChange={onChange}
  onBlur={onBlur}
/>

Controller はこの接続を自動でやってくれます。


実務例

React Select

<Controller
  name="role"
  control={control}
  render={({ field }) => (
    <Select
      {...field}
      options={[
        { label: "Admin", value: "admin" },
        { label: "User", value: "user" },
      ]}
    />
  )}
/>

重要: 普通の inputonChangeevent.target.value を返しますが、React Select の onChangeselectedOption オブジェクト全体を返します。RHF に value だけを保存したい場合は明示的に変換が必要です。

<Controller
  name="role"
  control={control}
  render={({ field }) => (
    <Select
      value={field.value}
      onChange={(option) => field.onChange(option?.value)}
      options={[
        { label: "Admin", value: "admin" },
        { label: "User", value: "user" },
      ]}
    />
  )}
/>

field.onChange(option?.value) とすることで、RHF には "admin""user" という文字列だけが保存されます。

DatePicker

<Controller
  name="birthday"
  control={control}
  render={({ field }) => (
    <DatePicker
      selected={field.value}
      onChange={field.onChange}
    />
  )}
/>

エラー表示

Controller を使う場合でも、エラーは formState.errors から取得できます。

const {
  control,
  handleSubmit,
  formState: { errors },
} = useForm({
  defaultValues: { role: "", birthday: null },
});

<Controller
  name="role"
  control={control}
  rules={{ required: "役職を選択してください" }}
  render={({ field }) => (
    <>
      <Select
        value={field.value}
        onChange={(option) => field.onChange(option?.value)}
        options={[
          { label: "Admin", value: "admin" },
          { label: "User", value: "user" },
        ]}
      />
      {errors.role && (
        <p style={{ color: "red" }}>{errors.role.message}</p>
      )}
    </>
  )}
/>

よくあるミス

defaultValues を忘れる

Controller だけだと warning が出ることがあります。必ず useForm 側で設定しましょう。

useForm({
  defaultValues: {
    birthday: null,
    role: "",
  },
})

registerfield を混在させる

// NG — 二重管理になる
<Input {...register("email")} {...field} />

どちらか片方に統一してください。


useFieldArray

動的フォームを管理する Hook です。

  • 電話番号を複数追加
  • メンバーを複数追加
  • 複数住所・商品一覧・SNS リンク追加

実務で非常によく使います。

基本形

const { control, register } = useForm({
  defaultValues: {
    phones: [{ number: "" }],
  },
});

const { fields, append, remove } = useFieldArray({
  control,
  name: "phones",
});

実装例

{fields.map((field, index) => (
  <div key={field.id}>
    <input
      {...register(`phones.${index}.number`)}
      placeholder="電話番号"
    />
    <button type="button" onClick={() => remove(index)}>
      削除
    </button>
  </div>
))}

<button type="button" onClick={() => append({ number: "" })}>
  追加
</button>

なぜ field.id を使うのか

// ✅ 正しい
key={field.id}

// ❌ 危険
key={index}

key={index} を使うと、削除時に React が誤認識して値がズレることがあります。必ず field.id を使ってください。

エラー表示(useFieldArray)

const {
  control,
  register,
  formState: { errors },
} = useForm({
  defaultValues: {
    phones: [{ number: "" }],
  },
});

{fields.map((field, index) => (
  <div key={field.id}>
    <input
      {...register(`phones.${index}.number`, {
        required: "電話番号を入力してください",
        pattern: {
          value: /^[0-9-]+$/,
          message: "数字とハイフンのみ入力できます",
        },
      })}
      placeholder="電話番号"
    />
    {errors.phones?.[index]?.number && (
      <p style={{ color: "red" }}>
        {errors.phones[index].number.message}
      </p>
    )}
    <button type="button" onClick={() => remove(index)}>
      削除
    </button>
  </div>
))}

Controller + useFieldArray の組み合わせ

フィールドが動的で、かつ UI ライブラリを使う場合はこの組み合わせになります。

const { control, handleSubmit, formState: { errors } } = useForm({
  defaultValues: {
    members: [{ name: "", role: "" }],
  },
});

const { fields, append, remove } = useFieldArray({
  control,
  name: "members",
});

{fields.map((field, index) => (
  <div key={field.id}>
    {/* 通常の input は register で OK */}
    <input
      {...register(`members.${index}.name`, {
        required: "名前を入力してください",
      })}
      placeholder="名前"
    />
    {errors.members?.[index]?.name && (
      <p style={{ color: "red" }}>
        {errors.members[index].name.message}
      </p>
    )}

    {/* UI ライブラリのコンポーネントは Controller を使う */}
    <Controller
      name={`members.${index}.role`}
      control={control}
      rules={{ required: "役職を選択してください" }}
      render={({ field }) => (
        <>
          <Select
            value={field.value}
            onChange={(option) => field.onChange(option?.value)}
            options={[
              { label: "Admin", value: "admin" },
              { label: "User", value: "user" },
            ]}
          />
          {errors.members?.[index]?.role && (
            <p style={{ color: "red" }}>
              {errors.members[index].role.message}
            </p>
          )}
        </>
      )}
    />

    <button type="button" onClick={() => remove(index)}>
      削除
    </button>
  </div>
))}

<button
  type="button"
  onClick={() => append({ name: "", role: "" })}
>
  + メンバー追加
</button>

パフォーマンスの注意点

fields が多い場合、不要な再レンダリングを避けるために以下を検討してください。

  • React.memo でフィールドコンポーネントを分割する
  • useWatch で監視範囲を絞る
  • 各行を独立したコンポーネントに切り出す

大規模フォームではここが差になります。


まとめ

Hook / コンポーネント 用途
register 普通の HTML input
Controller UI ライブラリのコンポーネント
useFieldArray 動的に増減するフォーム

この 3 つで React Hook Form の 80% をカバーできます。特に ControlleruseFieldArray の組み合わせを押さえると、実務レベルのフォームはほぼ対応できます。


参考

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?