useState で set〇〇したときに 同じ値なら更新されない!?
はい、その認識で正しいです 👍
React の useState
は 新しい値が現在の値と異なる場合のみ再レンダリングをトリガーします。
つまり、
const [sample, setSample] = useState(1);
// ここで:
setSample(1);
と書いても、すでに sample
が 1
であれば 値が変化していないため、再レンダリングは行われません。
補足ポイント
-
React は
Object.is
を使って「前回の state」と「新しい state」を比較します。
そのためプリミティブ値(数値や文字列など)は単純に値が同じなら「変わっていない」と判断されます。 -
ただし、オブジェクトや配列などの参照型を渡した場合は 参照の違い が比較されるため、たとえ中身が同じでも新しいオブジェクトを渡せば再レンダリングされます。
例:
const [obj, setObj] = useState({ a: 1 });
// 新しい参照なのでレンダリングされる
setObj({ a: 1 });
👉 まとめると:
-
setSample(1)
→ 既に1
なら再レンダリングされない -
setObj({ a: 1 })
→ 中身が同じでも新しいオブジェクトなら再レンダリングされる
もし「同じ値でも強制的に再レンダリングしたい」ケースがあるなら、別の state をトリガー用に作るか、useReducer
で「更新カウンタ」を使う手もあります。
プリミティブな値とそうでない値の違い
useState
の挙動は 「新しい値」と「前回の値」が同じかどうかを Object.is
で比較する ことに基づいています。
そのため、プリミティブ型 と 参照型(オブジェクトや配列など) で挙動が変わります。
1. プリミティブ値(数値・文字列・真偽値など)
特徴
- 値そのものが直接比較される。
- 例えば数値
1
と1
は完全に同じとみなされる。
例
const [count, setCount] = useState(1);
// 値が同じなのでレンダリングされない
setCount(1);
// 値が違うのでレンダリングされる
setCount(2);
👉 プリミティブは「同じ値なら再レンダリングされない」。
2. オブジェクトや配列(参照型)
特徴
- 比較されるのは「中身」ではなく「参照(メモリアドレス)」。
- 新しいオブジェクトや配列を作ると、たとえ中身が同じでも別の参照として扱われる。
例(オブジェクト)
const [obj, setObj] = useState({ a: 1 });
// 中身は同じ { a: 1 } だが、参照が違う → レンダリングされる
setObj({ a: 1 });
// 参照が完全に同じ → レンダリングされない
const sameRef = obj;
setObj(sameRef);
例(配列)
const [arr, setArr] = useState([1, 2, 3]);
// 新しい配列なのでレンダリングされる
setArr([1, 2, 3]);
// 参照が同じなのでレンダリングされない
setArr(arr);
👉 参照型は「参照が変われば必ずレンダリングされる」。
3. よくある実務での混乱ポイント
-
プリミティブなら値が変わらない限りレンダリングされない。
-
setCount(count)
→ レンダリングされない。
-
-
参照型は「新しく生成した時点」でレンダリング対象になる。
-
setArr([...arr])
→ 中身が同じでもレンダリングされる。
-
-
パフォーマンス最適化を考えるとき
-
useMemo
やuseCallback
を組み合わせないと、毎回新しいオブジェクトや配列を作って無駄に再レンダリングが走るケースがある。
-
4. まとめ表
型の種類 | 同じ値を渡した場合 | 新しい値を渡した場合 |
---|---|---|
プリミティブ | レンダリングされない | レンダリングされる |
オブジェクト/配列 | 参照が同じならされない | 参照が違えばされる |
再レンダリングを意図的に防ぐ? 意図せずに無駄にレンダリングされてしまう!
前提:useState
の比較は Object.is
-
同一とみなす:
1 === 1
,"a" === "a"
,NaN
とNaN
(←Object.is(NaN, NaN)
はtrue
) -
異なるとみなす:
0
と-0
(←Object.is(0, -0)
はfalse
) -
参照型は参照が違えば常に異なる:
{a:1}
と{a:1}
は別参照
① 同じ値でも「再レンダリングさせたい」場合
プリミティブ
パターンA:更新カウンタを別に持つ
const [value, setValue] = useState(1);
const [version, bump] = useReducer(v => v + 1, 0);
// 値は1のままにして画面だけ更新したい
setValue(1);
bump(); // 強制的に再レンダリング
パターンB:useReducer
を state 本体に使う
const [state, dispatch] = useReducer((s, action) => {
if (action.type === "set") return action.value; // 値更新
if (action.type === "force") return { ...s }; // 参照を変えて再描画
return s;
}, 1);
// 同じ値でも強制再描画
dispatch({ type: "set", value: 1 });
dispatch({ type: "force" });
オブジェクト/配列
- 新しい参照を渡すだけで再レンダリングされます。
const [obj, setObj] = useState({ a: 1 });
setObj({ ...obj }); // 中身同じでも参照が変わる → 再レンダリング
② 無駄な再レンダリングを避けたい場合
A. プリミティブ
- React が同値なら更新を無視しますが、無駄な
setState
呼び出し自体を避けるとキュー負荷減少。
if (next !== value) setValue(next);
-
注意:
0/-0
は異なると判定され得ます。符号ゼロを扱う可能性があれば正規化してから比較を。
B. オブジェクト/配列
- 本当に変えたときだけ新しい参照を作る(差分がなければ同じ参照を返す)。
setObj(prev => {
if (prev.a === nextA) return prev; // 参照を保つ → レンダリング抑制
return { ...prev, a: nextA }; // 変化時のみ新参照
});
- 配列更新も同様に:
setList(prev => {
const i = prev.findIndex(x => x.id === item.id);
if (i === -1) return prev;
if (prev[i].value === item.value) return prev; // 参照維持
const next = prev.slice();
next[i] = { ...prev[i], value: item.value };
return next;
});
C. 子コンポーネントへの“安定した参照”の受け渡し
-
React.memo
+useCallback
/useMemo
で props の同一性を保つ。
const onSave = useCallback(() => { /* ... */ }, [id]); // 安定ハンドラ
const columns = useMemo(() => makeColumns(schema), [schema]); // 安定配列
const Row = memo(function Row(props){ /* ... */ });
// areEqual でカスタム比較も可能
// memo(Row, (prev, next) => shallowEqual(prev.item, next.item))
- NG 例(毎回新参照で子が再描画):
<Row columns={makeColumns(schema)} onSave={() => save(id)} />
D. 状態のコロケーション & 重複 state を持たない
- ある state から導出できる値は 格納せず
useMemo
で導出:
const [items, setItems] = useState(data);
const expensiveView = useMemo(() => build(items), [items]); // 導出のみ
E. useRef
に“UIに影響しない可変値”を入れる
- タイマーID、前回値キャッシュ、計測カウンタ等は
useRef
→ 更新しても再描画されない。
const timerIdRef = useRef<number | null>(null);
timerIdRef.current = window.setTimeout(...); // レンダリングなし
F. コンテキストの過剰再描画を避ける
-
分割コンテキストや **selector 付きコンテキスト(
use-context-selector
等)**を検討。 - もしくは外部ストア(Zustand など)や
useSyncExternalStore
の selector で購読範囲を最小化。
G. リスト最適化の実践
-
key
は 安定 ID を使う(index は並べ替えで再描画を招く)。 - 大量描画は バーチャルリスト(
react-virtualized
,react-window
)。 - 入力連打などは デバウンス/スロットルし、
useDeferredValue
で重い子描画を遅延。
典型アンチパターン → 置き換え例
悪い
// 1) 変更有無に関係なく毎回新配列を作る
setFilters([...filters, cond]); // cond が既に含まれるか未確認
// 2) JSX 内で毎回新しいオブジェクトを作る
<Chart options={{ theme, legend: true }} />
良い
setFilters(prev => {
if (prev.includes(cond)) return prev; // 参照維持
return [...prev, cond];
});
const options = useMemo(() => ({ theme, legend: true }), [theme]);
<Chart options={options} />
速攻チェックリスト
-
プリミティブ:
next !== cur
を満たすときだけsetState
- オブジェクト/配列: 変更がないなら 同じ参照を返す
-
子への props:
useMemo
/useCallback
で 参照安定化、子はmemo
-
導出値: state に持たず
useMemo
-
非 UI 値:
useRef
-
大量描画/高頻度更新: デバウンス/バーチャル化/
useDeferredValue
どこで React.memo / useMemo / useCallback を使うべきか・使わなくていいか
サンプル構造(Before)
// 親コンポーネント
function Parent({ schema, theme, id }: { schema: Schema; theme: string; id: number }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Row
schema={schema}
theme={theme}
onSave={() => save(id)} // ← 毎回新しい関数
columns={makeColumns(schema)} // ← 毎回新しい配列
options={{ theme, legend: true }} // ← 毎回新しいオブジェクト
count={count}
/>
</div>
);
}
function Row({ schema, theme, onSave, columns, options, count }: RowProps) {
console.log("Row render");
return (
<div>
<h3>{schema.title}</h3>
<p>count: {count}</p>
<button onClick={onSave}>保存</button>
</div>
);
}
問題点
-
onSave
は毎回新しい関数 → 子が毎回レンダリングされる -
columns
は配列を毎回生成 → 毎回新参照 -
options
も毎回オブジェクトを生成 → 毎回新参照 -
Row
自体は「props が同じなら再レンダリング不要」なのに無駄に再描画される
改善後(After)
import React, { memo, useMemo, useCallback, useState } from "react";
function Parent({ schema, theme, id }: { schema: Schema; theme: string; id: number }) {
const [count, setCount] = useState(0);
// ✅ useCallback → id が変わらない限り onSave は安定
const onSave = useCallback(() => {
save(id);
}, [id]);
// ✅ useMemo → schema が変わらない限り columns は再利用
const columns = useMemo(() => makeColumns(schema), [schema]);
// ✅ useMemo → theme が変わらない限り options は再利用
const options = useMemo(() => ({ theme, legend: true }), [theme]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Row
schema={schema}
theme={theme}
onSave={onSave}
columns={columns}
options={options}
count={count}
/>
</div>
);
}
// ✅ React.memo → props が変わらない限り Row は再レンダリングされない
const Row = memo(function Row({ schema, theme, onSave, columns, options, count }: RowProps) {
console.log("Row render");
return (
<div>
<h3>{schema.title}</h3>
<p>count: {count}</p>
<button onClick={onSave}>保存</button>
</div>
);
});
ポイント解説
✅ memo
をつけるべき場所
- Row のように「props が重い・大きい・処理が重い」子コンポーネント。
- 逆に、表示が小さい・軽いコンポーネントには
memo
は不要(オーバーヘッドの方が高い)。
✅ useMemo
が必要な場所
-
配列やオブジェクトを毎回作る props
-
makeColumns(schema)
のように計算コストがある場合 -
{ theme, legend: true }
のようにオブジェクトリテラルを直書きする場合
-
-
ただし、計算が軽い場合(数値の2倍とか) は不要。React が比較してくれるので、気にしすぎるとコードが読みにくくなる。
✅ useCallback
が必要な場所
-
子に関数を渡すとき
- その子が
memo
化されている場合のみ有効。
- その子が
-
memo
化されていないなら毎回レンダリングするのでuseCallback
は不要。
まとめ(判断フロー)
- 子を
memo
するなら → 渡す props の参照を安定化(useMemo
/useCallback
)する - 子を
memo
しないなら →useMemo
/useCallback
は原則不要 -
計算が重い処理 or オブジェクト/配列生成 →
useMemo
-
イベントハンドラを子に渡す →
useCallback