Reactの「Cannot update a component while rendering a different component」を修正する

intermediate⚛️ React2026-05-16| React 16.13+、React 17、React 18 — あらゆるブラウザ・OS対応

Error Message

Warning: Cannot update a component (`App`) while rendering a different component (`Child`). To locate the bad setState() call inside `Child`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
#react#setState#レンダリング#副作用#警告

エラーの内容

ブラウザのコンソールを開くと、通常は何度も繰り返される次のようなメッセージが表示されます:

Warning: Cannot update a component (`App`) while rendering a different component (`Child`).
To locate the bad setState() call inside `Child`, follow the stack trace as described in
https://reactjs.org/link/setstate-in-render

アプリはまだ動作しているかもしれません。しかしReactは本当に危険な状態を警告しています:子コンポーネントがレンダリングフェーズ中に親コンポーネントの状態更新をトリガーしています。React 16.13でこの警告が導入されました。React 18のコンカレントモードでは、実際のクラッシュに発展する可能性があります。

根本原因

Reactのレンダリングフェーズは純粋な計算でなければなりません。副作用、状態の更新、関数の外部に影響を与えるいかなる変更も許されません。Childがイベントハンドラやエフェクトの中ではなく、レンダリングボディの中で直接Appの状態を設定するプロップコールバックを呼び出すと、ReactがまだレンダリングしているさなかにStateの更新をスケジュールしてしまいます。これはReactの規約違反です。

次の3つのパターンが最も多く発生します:

  • コンポーネントの先頭で親のセッター関数を無条件に呼び出す
  • プロップとして渡されたレンダリングフェーズのコールバック内でsetStateを呼び出す
  • refコールバックまたはレンダープロップが同期的にstate更新を発火する

バグの再現

以下は警告を引き起こす最小限のケースです:

// ❌ 壊れている — ChildはエフェクトではなくレンダリングでonMountを呼び出す
function Child({ onMount }) {
  onMount('Child is here'); // レンダリング中に実行される!
  return <div>Child</div>;
}

function App() {
  const [message, setMessage] = React.useState('');

  return (
    <div>
      <p>{message}</p>
      <Child onMount={setMessage} />
    </div>
  );
}

AppがレンダリングするたびにChildもレンダリングされます。Childは即座にAppのstateセッターであるsetMessageを呼び出します。これによりAppの再レンダリングがキューに入ります。Reactはこれを途中で検知して警告をログに記録します。最悪の場合、無限レンダリングループが発生します。

修正手順

修正1:useEffectの中に移動する(最も一般的な修正)

子がマウントされたときや値が変わったときに親に通知するには、その呼び出しをuseEffectの中に入れます:

// ✅ 修正済み — 副作用はレンダリング中ではなくレンダリング後に発生する
function Child({ onMount }) {
  React.useEffect(() => {
    onMount('Child is here');
  }, []); // 空の依存配列 = マウント後に一度だけ実行

  return <div>Child</div>;
}

function App() {
  const [message, setMessage] = React.useState('');

  return (
    <div>
      <p>{message}</p>
      <Child onMount={setMessage} />
    </div>
  );
}

useEffectはブラウザが描画した後に実行されます。親のstate更新は次のサイクルで行われるため、レンダリングの競合も警告も発生しません。

修正2:stateを同期させる代わりに導出する

この警告はコードの問題を示している場合があります。プロップや既存のstateから単純に導出できるstateを手動で同期させていることが明らかになります。Childが報告しようとしているデータをAppがすでに持っていないか考えてみましょう:

// ❌ アンチパターン:子がレンダリング中にコールバックでデータを親に返す
function Child({ value, onValueSeen }) {
  onValueSeen(value); // 警告を引き起こす
  return <span>{value}</span>;
}

// ✅ より良い方法:親がデータを管理し、子はそれを表示するだけ
function App() {
  const [value] = React.useState('hello');
  // Childが報告する必要はない — Appはすでに`value`を持っている
  return <Child value={value} />;
}

修正3:stateを子コンポーネントに移動する

一つのコンポーネントだけが必要とするstateは親に置くべきではありません。子に移動させましょう:

// ❌ 親が子だけが使うstateを保持している
function App() {
  const [isOpen, setIsOpen] = React.useState(false);
  return <Modal isOpen={isOpen} onToggle={setIsOpen} />;
}

// ✅ 子が自身の開閉stateを管理する
function Modal() {
  const [isOpen, setIsOpen] = React.useState(false);
  return (
    <div>
      <button onClick={() => setIsOpen(o => !o)}>Toggle</button>
      {isOpen && <div>Modal content</div>}
    </div>
  );
}

修正4:サードパーティコンポーネントのコールバックを確認する

ライブラリが原因の場合もあります。データグリッド、フォームライブラリ、仮想化リストは自身のレンダリングサイクル中にコールバックを呼び出すことがあります。よくある問題のケース:

// ❌ onRowsChangeがDataGridのレンダリングサイクル中に発火する
<DataGrid
  rows={rows}
  onRowsChange={(newRows) => setRows(newRows)} // レンダリング中に発火する可能性がある
/>

// ✅ useCallbackでラップし、ライブラリのドキュメントを確認する
const handleRowsChange = React.useCallback((newRows) => {
  setRows(newRows);
}, []);

<DataGrid rows={rows} onRowsChange={handleRowsChange} />

レンダリング中に常に発火するライブラリにはバグがあります。新しいバージョンを確認するか、useEffectでstate更新を遅延させましょう。

デバッグ戦略

警告には関係するコンポーネントが正確に示されています。これを活用しましょう:

  • DevToolsを開く → コンソール
  • 警告の横にあるスタックトレースリンクをクリックする
  • Childコンポーネント内のフレームを見つける(Reactの内部処理はスキップ)
  • その行でsetStateが発火している — イベントハンドラ、useEffect、またはレンダリングボディのどこにあるか確認する

スタックトレースが縮小されている場合は、開発ビルドで実行してください(npm startまたはnext dev)。Reactはこの警告を開発モードでのみ出力します — プロダクションビルドでは表示されません。

修正の確認

修正を適用した後、以下のチェックリストを確認してください:

  • ページをハードリフレッシュする(Ctrl+Shift+R / Cmd+Shift+R
  • コンソールを開く — 警告が消えているはずです
  • 影響を受けるフローを確認する:子コンポーネントのマウント、更新、アンマウント
  • React DevTools Profilerで、カスケードする再レンダリングなしにレンダリング回数が安定していることを確認する

React 18で注意すべき点があります:Strict Modeは開発環境でマウント時にエフェクトを意図的に2回実行します。useEffectの修正によって親のセッターが2回発火する場合は、クリーンアップフラグを追加してください:

React.useEffect(() => {
  let active = true;
  if (active) onMount('Child is here');
  return () => { active = false; };
}, []);

学んだこと

  • **レンダリングは純粋でなければならない。**コンポーネントのトップレベルで、直接またはコールバックプロップを通じてsetStateを呼び出してはいけません。それはイベントハンドラとuseEffectの役割です。
  • プロップコールバックはイベントハンドラではない。setFooをプロップとして渡しても、レンダリング中に安全に呼び出せるわけではありません。受け取ったコンポーネントがいつ発火するかを制御します。
  • **これを警告ではなくエラーとして扱う。**React 18のコンカレントモードでは、レンダリングフェーズの副作用が非決定論的になります。今日の無害な警告が、コンカレント機能が有効になった際にstateを静かに破壊する可能性があります。
  • **マウント時の通知にはデフォルトでuseEffectを使用する。**子がマウント時に親に何かを伝える必要がある場合、空の依存配列を持つuseEffectが適切なツールです。

Related Error Notes