useFieldArrayで遭遇したレアなバグたち

フロントエンド開発者であれば、React Hook Form がフォームバリデーションの頼れるライブラリの一つであることに異論はないでしょう。特に、フィールドの追加・削除・並べ替えといった動的な操作が必要な複雑なフォームでは、その真価を発揮します。その強みの一つは、変更を的確に追跡しつつ不要な再レンダリングを避けられる点です。しかし、まさにその強みが、思わぬバグを引き起こすこともあります。

今回は、ベトナムでエンジニアを担う私が実際に遭遇したuseFieldArrayに関する2つのバグと、その解決方法を紹介します。

1. バグ #1

ネストされたコンポーネント内の useFieldArray

💥 何が起きたのか

Googleフォームのような、ユーザーが質問を追加したり、回答の種類を変更できる柔軟なフォームを構築していました。

このフォームには 2 層の useFieldArray がありました:

  • useFieldArray #1:質問の配列

  • useFieldArray #2:各質問の中にある回答の配列

問題は、質問を追加してから削除すると、質問や回答のテキストが編集できなくなるというものでした。入力欄が無反応になってしまうのです。

🤯 当時の考え

正直、最初は困惑しました。ドキュメントにあるネストされた useFieldArray の例とほぼ同じ構造だったからです。違いといえば、ドキュメントでは直接入力registerを使っていたのに対し、自分は Controller を使っていたことくらいです。

ドキュメントの例(直接バインド):

<input
 placeholder="first name"
 {...register("firstName")}
/>

自分の例(Controller 使用):

<Controller
 control={control}
 name="firstName"
 render={({ field, fieldState: { error } }) => (
  <input
   {...field}
   placeholder="first name"
   error={error?.message}
   />
  )}
/>

実際のフィールド名はもっと深く、このようなものでした:

content.${questionIndex}.answers.${answerIndex}.answerText

✅ 解決方法

Controller 内の <input />{...field} を渡しているのにお気づきでしょうか?
実はこちら、公式ドキュメントには明示されていない書き方でした。

他の useFieldArray ではこれでうまくいっていたのに、このケースでは動かない。おそらく、質問を中間位置で削除したときに、React Hook Form がネストされたフィールドの状態を正しくリセットできず、Controller が変更を正しく検知できなかったのだと思います。

そこで試したのが、Controller 内でも 明示的に register() を呼び出すことです:

<input
 {...register(`content.${questionIndex}.answers.${answerIndex}.answerText`)}
/>

少し冗長に見えますが、これによりどのような変更後でも再レンダリングされることが保証され、問題が解決しました。正直なところ、まだ完全に仕組みを理解できているわけではありません。 それでも、動作したのでひとまずは良しとしています。

2. バグ #2

useFieldArray でのラジオボタン

💥 何が起きたのか

各アイテムに「名前」と「ラジオ選択で決まる値」があるシンプルなリストを作っていました。

ところが、アイテムを並び替えると、ラジオボタンの選択が反応しなくなるという問題が発生しました。

このケースはネストも浅く、原因も比較的すぐに見つかりました。ラジオの選択状態が変わらないときは、まず「checked が正しく設定されているか?」を疑うのが鉄則です🙂

✅ 解決方法

以下がバグのあったコードです:

{SELECTION_PROCESS_OPTIONS.map((option, optionId) => (
 <Controller
  key={optionId}
  name={`items.${statusIndex}.flowOrder`}
  control={control}
  render={({ field: { value } }) => (
   <Radio
    label={option.label}
    value={option.value}
    checked={value === option.value}
    {...register(`items.${statusIndex}.flowOrder` as const)}
   />
  )}
 />
))}

そして、修正後のコードがこちら:

{SELECTION_PROCESS_OPTIONS.map((option, optionIndex) => (
 <Radio
  key={optionIndex}
  label={option.label}
  value={option.value}
  checked={watchItems[statusIndex].flowOrder === option.value}
  {...register(`items.${statusIndex}.flowOrder` as const)}
  />
))}

Controller 経由で受け取っていた value古くなってしまい、更新されていなかったのが原因でした。
そこで、watch() で配列全体を監視し、リアルタイムな値を参照するようにしたところ、問題はきれいに解決しました。

3. 結論

この 2 つのバグから得た大きな学びは、Controller を使いすぎないことです。UI ライブラリとの連携では便利な一方で、特に useFieldArray のような動的なフィールドと組み合わせると、想定外の挙動を引き起こすことがあります。

もし Controller を使っていて値が正しく更新されないようであれば、一度 register() を試してみるのも手です。そして、ラジオボタンの checked 状態には要注意です。バグの原因がそこにあるケースは、意外と多いです。

バグの原因はコードの奥に潜んでいることもあります。小さな違和感を見逃さず丁寧に向き合うことが、安定した実装への近道かもしれません。

この情報は役に立ちましたか?


フィードバックをいただき、ありがとうございました!

関連記事

カテゴリー:

ブログ

情シス求人

  1. システム開発におけるテスト工程の重要性と各テストの役割

  2. チームメンバーで作字やってみた#1

ページ上部へ戻る