1
0

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 都市伝説「同じ値なら re-render されない」

1
Last updated at Posted at 2026-07-05

React 勉強中。 useEffect を学ぶところで useState がわからないところに戻り大混乱。

claude がこれは大丈夫っていうんだよね

function Loop() {
  const [count, setCount] = useState(0);
  setCount(count); // 同じ値
  return <p>{count}</p>;
}

setCount(count) みたいに同じ値を渡せば、Object.is で弾かれて bail out する。だから render 中に呼んでも loop しない。

  • bail out = React が「値が変わってないから更新する意味なし」と判断して、re-render を途中でやめること。飛行機から緊急脱出するのが bail out.

やったら無限ループw

Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

同じ値だとレンダリングされないって確かに聞いたことがあるので、基礎から勉強

やってみた。

都市伝説の実態

ツッコんだらclaude曰く「同じ値なら re-render されない」という一言に、3つの別物があるそうな

便宜的な名前 同じ値で止まる? 対象の setState 何を比較
MANUAL_STOP 自分で書けば止まる class の setState props / state(自分で比較)
OUTSIDE_RENDER 自動で止まる render で呼んだ setState Object.is(新, 旧)
DURING_RENDER 止まらない render で呼んだ setState 比較しない

renderingが止まらない方が多い。

MANUAL_STOP — class 時代は同じ値でも re-render してた

Hooks(useState たち)が入ったのは React 16.8、2019年2月。それ以前の React は class で component を書いていた。class は廃止されてなくて今も動く。

class 時代の setState は、同じ値を渡しても re-render する。値の比較すらしない。

class Counter extends React.Component {
  state = { count: 0 };
  handleClick = () => {
    this.setState({ count: this.state.count }); // 同じ値
  };
  render() {
    console.log("render"); // 押すたびに出る
    return <button onClick={this.handleClick}>{this.state.count}</button>;
  }
}

同じ値でも render() は走る。

止めたければ自分で書く。class には shouldComponentUpdate という method があって、re-render の直前に React が呼んでくる。ここで false を返すと render がスキップされる。

shouldComponentUpdate(nextProps, nextState) {
  // 次の state と今の state を自分で比較して、
  // 変わってないなら false = re-render しない
  return nextState.count !== this.state.count;
}

毎回これを書くのは面倒。なので「props と state をひと通り比較する shouldComponentUpdate」が最初から組み込まれた class も用意されていた。それが PureComponent

class Counter extends React.PureComponent {} // 比較が組み込み済み

つまり class 時代、「同じ値なら re-render しない」は React の標準動作じゃない。開発者が比較を手で書く(か PureComponent を選ぶ)opt-in 機能。だから MANUAL_STOP。

OUTSIDE_RENDER — 自動で止まるのはここだけ

function Counter() {
  const [count, setCount] = useState(0);
  console.log("render");
  return <button onClick={() => setCount(count)}>{count}</button>;
  //                             ^^^^^^^^^^^ 同じ値
}

この onClick は押しても re-render されない。React が更新前に Object.is(新, 旧) を比較して、等しければ更新を捨てる。これが bail out。

ん? じゃあ最初の Loop はなんで loop した? 同じ setCount(count) なのに。

答えは DURING_RENDER。先に traps を2つ。

trap 1: object と配列は中身が同じでも別物

setItems([]); // 毎回新しい空配列
// Object.is(前の[], 新しい[]) === false → 「変わった」扱い → re-render

Object.is は参照比較。中身は見ない。毎回 []{} を作って渡すと、bail out を毎回すり抜ける。

trap 2: この bail out、render 外でしか効かない

これが DURING_RENDER。

DURING_RENDER — Claude が間違えた場所

OUTSIDE_RENDER の自動 bail out は、名前の通り render 外専用。render 中に呼んだ setState には効かない

最初の LoopsetCount(count) を render 中に実行される関数に直書きしていた。 render 中の set は bail out されず loop する。

// render 中 → 効かない → Too many re-renders
function C() {
  const [c, setC] = useState(0);
  setC(c);
  return <p>{c}</p>;
}

// render 外(onClick)→ bail out 効く → re-render なし
return <button onClick={() => setCount(count)}>same</button>

1回のレンダリングには "始まりと終わり" が存在する

React が "C() を呼んでから JSX を return するまで" が render 中。関数本体に直書きした setC(c) はこの最中に実行される。

onClick の無名関数は、render 中には定義されて React に渡されるだけで、実行されない。実行されるのは render が終わったあと、click が起きたとき。だから render 外useEffect のコールバックや setTimeout、通信の完了も全部 return より後に発火するので render 外。

図にするとこう。

  • render 中の setState(render-phase update)は Object.is の早期 bail out 判定を通らない
  • 値が同じでも毎回「もう一回 render しろ」の要求が立つ
  • 25回で React が無限 loop と判断して止める
  • bail out 判定は render 外の通常更新経路にしか入っていない

「同じ値か」じゃなくて「一度のレンダリングの始まりから終わりまでの間にsetStateをしたか」。

render 中の setState は全部ダメなのか

聞いたら「条件で囲めば OK」。React 公式の "storing information from previous renders" パターン。

function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);

  if (prevCount !== count) {   // 「変わったときだけ」を自分で書く
    setPrevCount(count);       // 覚えてる値を更新。次は prevCount === count
    setTrend(count > prevCount ? "increasing" : "decreasing");
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </>
  );
}

やってることは、DURING_RENDER で効かなかった bail out の手書き。つまりこれも MANUAL_STOP の一種。class 時代は shouldComponentUpdate で、render 中は if で、自分で比較して自分で止める。

  • if (prevCount !== count) が「同じなら止める」の代わり
  • setPrevCount(count) で次の render の条件が false になり、loop が止まる

setPrevCount(count) を忘れると条件がずっと true で loop する。公式も "re-render in a loop until it crashes" と書いてる。最初に見た Too many re-renders と同じ結末。

公式いわく、このパターン自体、避けたほうがいいとのこと。

This pattern can be hard to understand and is usually best avoided.

useState – Storing information from previous renders

つまり OK 版も含めて、render 開始〜終了の間の setState は基本やらない。代わりは:

  • render 中に計算できるなら state に持たない(const fullName = firstName + lastName で済むなら setState 不要)
  • event handler(つまりrenderの外) で更新する

じゃあどうすればいいの

一番よくあるのは「default 値で表示して、初回に API で書き換える」パターン

const [data, setData] = useState(null); // default 値

useEffect(() => {
  fetch("/api/user").then(r => r.json()).then(setData);
}, []); // ← mount 時に1回だけ

loop しない理由をさっきの表に当てはめる。

  • effect は render 外で実行される → DURING_RENDER ではない
  • ただし API の結果は毎回新しい object。trap 1 の通り Object.is は毎回 false で、bail out は守ってくれない
  • 止めているのは []。mount 時に1回だけ、という有限回の保証

「useEffect だから安全」ではない。[] を忘れると effect が毎 render 走って "Maximum update depth exceeded"。守っているのは依存配列。

setTimeout は代替にならない。

// BAD: render 中に毎回 timer を予約 → ゆっくり無限 loop
function C() {
  const [data, setData] = useState(null);
  setTimeout(() => fetch("/api").then(r => r.json()).then(setData), 0);
  return <p>{data}</p>;
}

crash はしないが、render のたびに予約 → setState → re-render → また予約、で回り続ける。使うにしても置き場所は useEffect(..., []) の中。

実務では自前で書かずに TanStack Query / SWR、Next.js なら Server Component の fetch。理屈は全部同じ、「fetch を render の外・有限回に閉じ込める」。

useEffect の本質, 大事なこと

「useEffect は loop を防ぐ仕組み」ではなく 「render が終わったあとに実行する」もの。言い換えると「副作用を render の外に出す仕組み。loop を止めているのは依存配列」。

おまけ: React 18 以降 re-render タイミングが複雑に

Claude が挙げた3つ。

automatic batching

function onClick() {
  setA(1);
  setB(2);
  setC(3);
} // render は3回じゃなく1回

呼んだ回数 = render 回数、ではない。18からは setTimeout や Promise の中でもまとめられる。

Strict Mode の二重呼び出し(開発時のみ)

const renderCount = useRef(0);
renderCount.current += 1; // 開発時は1操作で +2 に見えることがある

React がわざと2回呼ぶ。狙いは、DURING_RENDER のような render 中の副作用の炙り出し。本番では起きない。

Concurrent Rendering

render は中断・破棄・やり直しされうる。1操作 = 1 render、も保証なし。

まとめ: この順で見る

「同じ値か?」で即答しない。

1. どのAPI?
   class setState              → 同じ値でも re-render する(止めたいなら MANUAL_STOP を書く)
   Hooks useState / useReducer → 2 へ

2. どこで呼んだ?
   render 外(event / effect) → Object.is が同じなら bail out(OUTSIDE_RENDER)
   render 中                   → bail out 効かない(DURING_RENDER)
                                 無条件なら Too many re-renders
                                 合法にするなら if (prev !== next) { setPrev(next); ... }

3. どの mode?(回数を数えるとき)
   Strict Mode(開発)  → 2回呼ばれる
   automatic batching  → 複数 setState が1 render にまとまる
   Concurrent          → 中断・破棄・やり直しあり

「同じ値なら止まる」が成立するのは、Hooks で・render の外で・参照が本当に同じとき。それだけ。

AIが言ってきたことは試してみないとね。

参考

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?