React で setState しても画面が更新されない
state は同一参照のオブジェクト / 配列を渡しても再レンダリングされない。
スプレッドで新しい参照を作るか、関数型 setState で前回値から派生させる。
クロージャ越しの古い state も典型的な原因。
#react#state#hooks#immutability#rerender
要約
setState を呼んでも画面が更新されないとき、原因は概ね 同一参照を渡している か 古い state をクロージャ経由で読んでいる の 2 系統。
前者は新しいオブジェクトを作って渡す、後者は関数型 setCount(c => c + 1) で前回値から派生させると解決する。
よくある原因
- state を mutate して同じ参照を渡す:
state.items.push(x); setState(state)のように既存配列を書き換えても、React はObject.isで参照比較するため再レンダリングしない - stale closure: イベントハンドラやタイマー内の
countが古いレンダリングの値を掴んでいて、続けてsetCount(count + 1)を呼んでも 1 しか進まない - 自動バッチング: React 18 以降は
PromiseやsetTimeout内も含めて自動でバッチされ、複数setCount(count + 1)が同じ値で潰れる - 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 の中に閉じ込める。