できない.dev

React で setState しても画面が更新されない

state は同一参照のオブジェクト / 配列を渡しても再レンダリングされない。
スプレッドで新しい参照を作るか、関数型 setState で前回値から派生させる。
クロージャ越しの古い state も典型的な原因。

#react#state#hooks#immutability#rerender

要約

setState を呼んでも画面が更新されないとき、原因は概ね 同一参照を渡している古い state をクロージャ経由で読んでいる の 2 系統。
前者は新しいオブジェクトを作って渡す、後者は関数型 setCount(c => c + 1) で前回値から派生させると解決する。

よくある原因

  1. state を mutate して同じ参照を渡す: state.items.push(x); setState(state) のように既存配列を書き換えても、React は Object.is で参照比較するため再レンダリングしない
  2. stale closure: イベントハンドラやタイマー内の count が古いレンダリングの値を掴んでいて、続けて setCount(count + 1) を呼んでも 1 しか進まない
  3. 自動バッチング: React 18 以降は PromisesetTimeout 内も含めて自動でバッチされ、複数 setCount(count + 1) が同じ値で潰れる
  4. render 中の更新: render 中に setState を呼ぶと無視される(または無限ループ警告になる)

解決策

1. 新しい参照を作って渡す

// NG: 同じ配列を返している
setItems(items => {
  items.push(newItem);
  return items;
});
 
// OK: 新しい配列を作る
setItems(items => [...items, newItem]);

オブジェクトも同様にスプレッドで複製する。

2. 関数型 setState

// NG: count が stale closure
const handle = () => {
  setCount(count + 1);
  setCount(count + 1); // 結果は +1 だけ
};
 
// OK: 前回値から派生
const handle = () => {
  setCount(c => c + 1);
  setCount(c => c + 1); // +2 進む
};

公式の Queueing a Series of State Updates でも、連続更新は関数型推奨。

3. 最新値が必要なら useRef

const latestRef = useRef(count);
latestRef.current = count;
 
const onClick = () => {
  setTimeout(() => {
    console.log(latestRef.current); // 常に最新
  }, 1000);
};

state は描画用、ref は最新値の読み取り用と役割を分ける。

4. レンダリング中に setState しない

// NG: 無限ループ
function App() {
  const [n, setN] = useState(0);
  setN(n + 1); // 毎レンダリングで呼ばれる
  return <div>{n}</div>;
}

副作用は useEffect の中に閉じ込める。

この記事は役立ちましたか?