ReactVituoso を使うと ReactHookForm FieldArray の状態が適切に更新されない
概要
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
virtuaを使うと問題なくいけてそうには見えました
ライブラリの内部的なつくりの違いに起因しているんでしょうね…