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

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


---

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

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

例えば、`react-hook-form` の `setValue` や `useState` の更新関数などを、コンポーネント関数のトップレベル（他のフック呼び出しや JSX の間）で直接呼び出すケースです。

---

## なぜループするのか？

```mermaid
sequenceDiagram
    participant R as React
    participant C as Component Render
    participant S as setValue / setState

    R->>C: レンダリング開始
    C->>S: レンダリング中に状態更新！
    S-->>R: 状態変わったよ！
    R->>C: じゃあ再レンダリングだ！
    C->>S: また状態更新！
    S-->>R: また状態変わった！
    R->>C: また再レンダリング！
    Note over R,C: これが無限に続く → エラー
```

React は画面を描画するためにコンポーネント関数を実行します。その実行**中に**状態が変更されると、「おっと、状態が変わったから、もう一回最初から描画し直さないと！」となり、再びコンポーネント関数が実行されます。これが繰り返されるのが無限ループの原因です。

---

## 安全なフォーム更新パターン

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

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