🗂

ReactVituoso を使うと ReactHookForm FieldArray の状態が適切に更新されない

2024/05/21に公開2

概要

ReactHookForm を使ってフォーム開発をしている。フォームの中には、useFieldArray を使った箇所があり、ユーザー操作によって任意の項目を追加できる。

項目の追加には上限はなく、ユーザーによっては50個や100個も追加しているケースがある。ここで問題になってくるのがパフォーマンス。レンダリングにやや時間がかかるようになる。

そこで、 ReactVirtuoso を利用して、いわゆる仮想リストとして描画することにした。
仮想リスト自体は問題なく動いているが、ReactVirtuoso で囲まれた部分の FieldArray の状態が更新されない、という問題が発生した。

利用したライブラリのバージョンは以下の通り。

  • react: "18.2.0"
  • react-hook-form: "^7.45.4"
  • react-virtuoso: "^4.7.10"

事象

起こっている事象をより詳しく説明。

今回作成したフォームでは、削除ボタンを押してフォーム項目を減らすことができる。つまり、FieldArray の remove メソッドを実行している。

これによって FieldArray の件数は減っているはずだが、いざフォーム全体の handleSubmit メソッドを実行した時に FieldArray の件数がおかしい。remove 前の状態になっていた。

調査

原因を調査。

まずは愚直に「ReactHookForm ReactVirtuoso」などと検索。以下の issue がヒットした。

issue: Items are not correctly removed from field array #9496 - github.com

まさしく同じような現象。
どうやら、ReactVirtuoso がデータをメモ化していることに起因するらしい。

ちなみに、自分のコードは以下のような感じ。だいぶ簡素化している。

import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { useForm, useFieldArray, Control } from "react-hook-form"
import { Virtuoso } from 'react-virtuoso'

export const App = () => {
    const { control, register } = useForm()
    const { fields, append, remove } = useFieldArray({
        control,
        name: "managers",
    });

    return (
        <>
            <div>担当者を入力</div>
            <Virtuoso
                style={{ height: '400px' }}
                data={fields}
                itemContent={index => <FormItem index={index} register={register} /> }
            />
            <button onClick={append}>追加</button>
            <button onClick={remove}>削除</button>
        </>
  )
}


const FormItem = ({ index, register }) => {
    // fields が0件の時にもなぜか FormItem がレンダーされていた!
    return (
        <input {...register(`managers.${index}`)} />
    )
}

コードコメントにも書いたとおり、削除ボタンを押して fields.length が0になったときにも FormItem コンポーネントがレンダーされていた。
そして、レンダーの際に register が実行され状態に齟齬が発生していた。これ以上は深く原因追求せず…。

対応

原因は分かったので愚直に対応。

fields の件数自体は正しいので、以下のようにコードを修正した。

// ...

export const App = () => {
    // ...

    return (
        <>
            // ...
            <Virtuoso
                // ...
                itemContent={index => <FormItem index={index} register={register} fields={fields} /> } // ← 新しくfields も渡す
            />
            // ...
        </>
  )
}


const FormItem = ({ index, register, fields }) => {
    if(fields.length ===0) {
        // これで意図せず register が呼ばれることを防ぐ
        return;
    }
    return (
        // ...
    )
}

あまりキレイなやり方ではないけど、いったん問題は解決した。

Discussion

nap5nap5

FieldArray の件数は減っているはずだが、いざフォーム全体の handleSubmit メソッドを実行した時に FieldArray の件数がおかしい。

virtuaを使うと問題なくいけてそうには見えました

tenkeitenkei

ライブラリの内部的なつくりの違いに起因しているんでしょうね…