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

useState で set〇〇したときに 同じ値なら更新されない!? useState 覚書き

Posted at

useState で set〇〇したときに 同じ値なら更新されない!?

はい、その認識で正しいです 👍

React の useState新しい値が現在の値と異なる場合のみ再レンダリングをトリガーします。
つまり、

const [sample, setSample] = useState(1);

// ここで:
setSample(1);

と書いても、すでに sample1 であれば 値が変化していないため、再レンダリングは行われません。


補足ポイント

  • 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. プリミティブ値(数値・文字列・真偽値など)

特徴

  • 値そのものが直接比較される。
  • 例えば数値 11 は完全に同じとみなされる。

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. よくある実務での混乱ポイント

  1. プリミティブなら値が変わらない限りレンダリングされない。

    • setCount(count) → レンダリングされない。
  2. 参照型は「新しく生成した時点」でレンダリング対象になる。

    • setArr([...arr]) → 中身が同じでもレンダリングされる。
  3. パフォーマンス最適化を考えるとき

    • useMemouseCallback を組み合わせないと、毎回新しいオブジェクトや配列を作って無駄に再レンダリングが走るケースがある。

4. まとめ表

型の種類 同じ値を渡した場合 新しい値を渡した場合
プリミティブ レンダリングされない レンダリングされる
オブジェクト/配列 参照が同じならされない 参照が違えばされる

再レンダリングを意図的に防ぐ? 意図せずに無駄にレンダリングされてしまう!

前提:useState の比較は Object.is

  • 同一とみなす: 1 === 1, "a" === "a", NaNNaN(←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 / useMemoprops の同一性を保つ。
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 など)や useSyncExternalStoreselector で購読範囲を最小化。

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

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