簡単に言うと React ぽくなくなったり 単純なフォームじゃなくなると useEffect だらけで辛くなるので そんなに積極的に使いたいものではないです
react-hook-form を使うと 内部で state を管理してくれて 再レンダリングを防げます
各 input が props ではなくコンテキスト内の form から値を取得するので 親の再レンダリングが不要になり 変更があった input のコンポーネント内だけが再レンダリングされます
表示内容が多いページで memo なしだと入力ごとに再レンダリングされる範囲が広くなり 重たくなったりしますが それを防げます
フォームの制御が特になく 項目が多いだけのフォームだと楽に使えて便利です
しかし 入力値の変化に応じてあちこちを変更する場合は面倒になってきます
useEffect(() => {
const subscription = watch((values, { name, type }) => {
if (name === "foo") {
setValue("bar", values.foo)
} else if (name === "bar") {
setState(values.bar)
}
})
return () => subscription .unsubscribe()
}, [watch])
みたいに callback を指定する watch を使い form の値や state を更新していくことになります
useEffect が必要です
更新された場所の名前はわかりますが 更新前の値はわかりません
変化した内容に応じて処理を変えたいなら自分で前の値を保持する仕組みを作っておかないといけません
また イベントハンドラ的な部分での処理ではなく render 関数中の処理で現在の form の値を使いたいなら 名前指定で watch を使います
const [foo, bar] = watch(["foo", "bar"])
この場合はどれが変わったのかわかりません
また 変わるたびに この watch を使ったコンポーネントが再レンダリングされるので useState に入れているのと変わらなくなります
ものによっては form の値のほとんどを watch に入れることになり useState と対して変わらないってこともあります
watch で取得した値を使うのが hook だと仕方ないですが JSX 部分なら input 以外でも form を参照するようにしてしまうのもありです
input のように controller を使って値を取得すれば watch にする必要がなく コンポーネント全体の再レンダリングを減らせます
input に使う以外の form とは関係ない値でも form に入れてしまい state にせず form で管理するのもありかもです
そうすれば form の処理だけに統一できますし 名前指定の watch を減らせます
それに コンポーネント間の共有でコンテキストのようにも使えます
親コンポーネントで form に関数を入れて 深い部分のコンポーネントでそれを参照します
ただしあくまでライブラリとしては form なのでこの使い方はいいのだろうか?という気はします
想定されたことを外れると将来的なアップデートで面倒な目に合うのは十分考えられます
そういう使い方のものを別に用意したほうがいいのかもという気はしてます
深い部分で watch を使う注意点ですが 使ったコンポーネントだけが再レンダリングされそうに見えてしまいますがそうではないです
React の仕組み的にそういう手段はなく useForm を使ったコンポーネントが再レンダリングされます
その結果 その子孫コンポーネントも再レンダリングされてそうみえるだけです
こういうことをしてみるとわかりやすいです
const Input = ({ control, name }) => {
const { field } = useController({ control, name })
return <input {...field} />
}
const Child = ({ control, watch, name }) => {
const value = watch(name)
console.log("render Child", name)
return (
<div>
<div>input value: {value}</div>
<Input control={control} name={name} />
</div>
)
}
const MemoChild = React.memo(Child)
const Parent = () => {
const { watch, control } = useForm({ defaultValues: { a: "1", b: "2", c: "3" } })
console.log("render Parent")
return (
<div>
<Input control={control} name="a" />
<hr />
<Child control={control} name="b" watch={watch} />
<hr />
<MemoChild control={control} name="c" watch={watch} />
</div>
)
}
Parent で useForm して watch を Child に渡します
Child で watch していて その input を書き換えると 「render Parent」 もログに表示されます
Parent コンポーネントも再レンダリングされています
また Child をメモした版の MemoChild を使うと props の control, name, watch には変更がないため MemoChild コンポーネントは再レンダリングされません
結果 Parent は再レンダリングされるのに MemoChild は再レンダリングされず 「render Child c」 はログに表示されません
画面上でも input の現在の状態を表示しているのですが ここが更新されず初期状態のままです
また props や state を見て ちょっと加工するというだけでも useEffect で setValue が必要になります
普通の React なら render 関数内で計算したり JSX 内に式を入れるくらいで済むことのために useEffect が増えていくのは見通しが悪くなるので不満です
さらに問題になるのはあちこちで同じ値を更新したいときです
useEffect やイベントハンドラの中で setValue することになりますが 特に useEffect だと呼び出し順がわかりづらいです
あちこちのコンポーネントから setValue されるとどういう順で呼び出されて値が変わるのかデバッグしづらく 思い通り動かないことがけっこうありました
通常の React なら useEffect は極力なくせて render 関数内での処理です
親から子の順ですし レンダリングごとにイミュータブルのはずなので 困ることは少ないです
完全に独立した入力項目で 初期値以外は外部から変更されることがないような場合はシンプルに書けて良さそうですが 制御する部分が多いと不満点が目立ちます
やっぱり 素の React で state 管理してる方が見やすくていい気がしますね