3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactの無限ループ「Too many re-renders」を防ぐ

Posted at

要点

  • Reactコンポーネントのレンダリング中にフォームの状態(setValue等)を直接変更すると無限ループ (Too many re-renders) の原因になる。
  • 状態変更は、イベントハンドラ (ユーザー操作起因) か useEffect (副作用) で行うのが原則。

よくあるハマりどころ:レンダリング中の副作用

React コンポーネントが画面を描画する処理(レンダリング)の最中に、そのコンポーネント自身の状態を変更しようとすると、問題が発生します。

例えば、react-hook-formsetValueuseState の更新関数などを、コンポーネント関数のトップレベル(他のフック呼び出しや 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>
  );
}

まとめ:安全にフォームを更新するために

  1. レンダリング中に状態を変えない: コンポーネント関数のトップレベルで setValuesetState を直接呼ばない。
  2. 副作用は useEffect で: データ取得後や特定の状態変化後にフォーム値を更新したい場合は useEffect を使う。
  3. ユーザー操作起因はイベントハンドラで: ボタンクリックなどで値をセットする場合は、対応するイベントハンドラ関数内で setValue を使う。
  4. 条件付きフェッチ: useSWRreact-query を使う際、不要なデータ取得を防ぐために、取得条件をチェックしてキーを動的に null にするなどの工夫をする。

同様の問題に直面した人が根本原因と解決策を理解しやすくなれば幸いです!

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?