要点
- Reactコンポーネントのレンダリング中にフォームの状態(
setValue
等)を直接変更すると無限ループ (Too many re-renders
) の原因になる。 - 状態変更は、イベントハンドラ (ユーザー操作起因) か
useEffect
(副作用) で行うのが原則。
よくあるハマりどころ:レンダリング中の副作用
React コンポーネントが画面を描画する処理(レンダリング)の最中に、そのコンポーネント自身の状態を変更しようとすると、問題が発生します。
例えば、react-hook-form
の setValue
や useState
の更新関数などを、コンポーネント関数のトップレベル(他のフック呼び出しや JSX の間)で直接呼び出すケースです。
なぜループするのか?
React は画面を描画するためにコンポーネント関数を実行します。その実行中に状態が変更されると、「おっと、状態が変わったから、もう一回最初から描画し直さないと!」となり、再びコンポーネント関数が実行されます。これが繰り返されるのが無限ループの原因です。
安全なフォーム更新パターン
NGパターン: レンダリング中の直接呼び出し
function MyComponent() {
const { watch, setValue } = useForm();
const lookupKey = watch("lookupKey");
// データ取得 (useSWR や react-query など)
const { data } = useSomeDataFetchingHook(lookupKey);
// 💥 NG: レンダリング中に直接 setValue を呼んでいる!
if (data) {
setValue("derivedValue", data.someResult);
}
return (/* ...JSX... */);
}
OKパターン: useEffect
で副作用として更新
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import useSWR from 'swr'; // 例として useSWR
// ダミーの関数 (実際はAPIクライアントなど)
declare function fetchData(key: string | null): Promise{ someResult: string } | null>;
declare function isValidKey(key: string | undefined): boolean;
function MyComponent() {
const { watch, setValue } = useForm{ lookupKey: string, derivedValue: string }>();
const lookupKey = watch("lookupKey");
// 1. データ取得のキーを条件付きにする (無効なら null)
const shouldFetch = isValidKey(lookupKey);
const swrKey = shouldFetch ? ["dataKey", lookupKey] : null;
// データ取得
const { data } = useSWR(swrKey, ([, key]) => fetchData(key));
// 2. データ取得完了後 (data が変わった後) に副作用として setValue を実行
useEffect(() => {
// data が存在する場合のみフォーム値を更新
if (data) {
setValue("derivedValue", data.someResult);
console.log("Form value set inside useEffect");
}
// 必要であれば、data が null や undefined になった場合に
// derivedValue をクリアする処理もここに追加できる
// else {
// setValue("derivedValue", "");
// }
}, [data, setValue]); // 依存配列: data または setValue が変わった時だけ実行
console.log("Rendering component...");
return (
form>
input {...register("lookupKey")} placeholder="Lookup Key (e.g., >3 chars)" />
input {...register("derivedValue")} placeholder="Derived Value" readOnly />
{/* 他の要素 */}
form>
);
}
もう一つのOKパターン: イベントハンドラ内での呼び出し
function AnotherComponent() {
const { setValue } = useForm();
const handleResetClick = () => {
// ユーザー操作 (クリック) を起点とした状態変更はOK
console.log("Button clicked, setting value...");
setValue("someField", "Reset Value");
};
return (
button type="button" onClick={handleResetClick}>
Reset Field
button>
);
}
まとめ:安全にフォームを更新するために
-
レンダリング中に状態を変えない: コンポーネント関数のトップレベルで
setValue
やsetState
を直接呼ばない。 -
副作用は
useEffect
で: データ取得後や特定の状態変化後にフォーム値を更新したい場合はuseEffect
を使う。 -
ユーザー操作起因はイベントハンドラで: ボタンクリックなどで値をセットする場合は、対応するイベントハンドラ関数内で
setValue
を使う。 -
条件付きフェッチ:
useSWR
やreact-query
を使う際、不要なデータ取得を防ぐために、取得条件をチェックしてキーを動的にnull
にするなどの工夫をする。
同様の問題に直面した人が根本原因と解決策を理解しやすくなれば幸いです!