React の再レンダリングを理解する

React の再レンダリングを理解する

投稿日: 2025年04月18日

学習振り返り
Tips
要約
  • Reactの再レンダリングは状態変更やpropsの変更により発生し、Virtual DOMを利用して最適化される。
  • 再レンダリングを最適化するためのテクニックには、React.memo、useCallback、useMemoがある。
  • 必要なときだけ最適化を行い、シンプルな実装を優先することが重要です。

React は現代の Web 開発において最も人気のあるライブラリの一つであり、その中核となる機能は「コンポーネント」と「レンダリング」のシステムです。 この記事では React の再レンダリングの仕組みを深く掘り下げていきたいと思います。(間違いなどがあれば指摘していただけるとありがたいです!)

目次

  • React の再レンダリングとは何か

  • 再レンダリングが発生するタイミング

  • 再レンダリングの問題点

  • 再レンダリングを最適化する

  • まとめ

React の再レンダリングとは何か

React におけるレンダリングとは、コンポーネントが実行されて、React 要素(Virtual DOM)を生成するプロセスです。再レンダリングとは、コンポーネントが何らかの理由で再度実行され、新しい React 要素ツリーを生成することを指します。

React の重要なポイントは、レンダリングと実際の DOM 更新が分離されているということです。レンダリングは Virtual DOM を生成するだけという事! 厳密にはもっと複雑なことが行われているかもしれませんが... (詳しい方いたら教えて欲しいです!!)

// このコンポーネントがレンダリングされると、
// Reactは以下のJSX(React要素)を生成
export default function Greeting({ name }) {
  return <h1>こんにちは、{name}さん!</h1>;
}

再レンダリングが発生するタイミング

React コンポーネントが再レンダリングされる主な理由は 3 つあります:

  1. 状態(state)の変更

    コンポーネント内でuseStateuseReducerなどを使って管理している状態が変更されると、そのコンポーネントは再レンダリングされます。

    export default function Counter() {
      const [count, setCount] = useState(0);
      console.log("再レンダリング");
    
      return (
        <div>
          <p>カウント: {count}</p>
          <button onClick={() => setCount(count + 1)}>増加</button>
        </div>
      );
    }
    

    上記の例では、ボタンがクリックされるたびにsetCountが呼び出され、Counterコンポーネントが再レンダリングされます。

  2. プロパティ(props)の変更

    親コンポーネントから渡される props が変更されると、子コンポーネントは再レンダリングされます。

    export default function Parent() {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>親の状態を変更</button>
          <Child value={count} />
        </div>
      );
    }
    
    function Child({ value }) {
      console.log("子コンポーネントがレンダリングされました");
    
      return <p>子コンポーネントの値: {value}</p>;
    }
    

    この例では、Parentコンポーネントの状態が変更されると、その状態がChildコンポーネントに props として渡されるため、Childコンポーネントも再レンダリングされます。

  3. 親コンポーネントの再レンダリング

    親コンポーネントが再レンダリングされると、デフォルトでは子コンポーネントも再レンダリングされます。これは、props が変更されていなくても発生します。

    export default function Parent() {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>カウント: {count}</p>
          <button onClick={() => setCount(count + 1)}>増加</button>
          <Child />
        </div>
      );
    }
    
    function Child() {
      console.log("子コンポーネントがレンダリングされました");
      return <p>子コンポーネント</p>;
    }
    

    この例では、Childコンポーネントは props を受け取っていませんが、Parentが再レンダリングされるたびにChildも再レンダリングされます。これは React のデフォルトの動作であることを理解しておきましょう!

再レンダリングの問題点

以下は、不必要な再レンダリングが発生する典型的なパターンです:

export default function Parent() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <div>
      <button onClick={() => setCount1(count1 + 1)}>Parent Count</button>
      <button onClick={() => setCount2(count2 + 1)}>Child Count</button>
      <p>Parent: {count1}</p>
      <Child count2={count2} />
    </div>
  );
}

function Child({ count2 }) {
  //重い処理
  let i = 0;
  while (i < 10000000) i++;
  return <p>Child: {count2}</p>;
}

上記の例だと、Child に非常に重い処理が含まれており、Parent Count ボタンを押すたびに Child コンポーネントが再レンダリングされ、重い処理が走ってしまいますのでその分パフォーマンスの悪化につながってしまいます。

再レンダリングを最適化する

React は、不必要な再レンダリングを防ぐための hook を提供しています:

1. React.memo

React.memo は、コンポーネントを「記憶する」ための機能です。簡単に言うと、「もし渡されるデータ(props)が前回と同じなら、わざわざ再描画しなくてもいいよ」と React に教えるものです。

export default function Parent() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  console.log("親コンポーネントがレンダリングされました");

  return (
    <div>
      <button onClick={() => setCount1(count1 + 1)}>Parent Count</button>
      <button onClick={() => setCount2(count2 + 1)}>Child Count</button>
      <p>Parent: {count1}</p>
      <Child count2={count2} />
    </div>
  );
}

const Child = React.memo(function Child({ count2 }) {
  console.log("子コンポーネントがレンダリングされました");
  //重い処理
  let i = 0;
  while (i < 10000000) i++;
  return <p>Child: {count2}</p>;
});

この例では、Parentcount1が変更されても、Childcount2が変更されない限りChildは再レンダリングされません。

2. useCallback

useCallbackは、依存配列が変更されない限り、関数の参照を保持するフックです。これにより、不必要な関数再作成を防ぎ、子コンポーネントへの安定した props を提供できます。

export default function Parent() {
  const [count, setCount] = useState(0);

  console.log("親コンポーネントがレンダリングされました");

  // count が変更されても同じ関数参照が保持される
  const handleClick = useCallback(() => {
    console.log("クリックされました");
  }, []); // 空の依存配列

  return (
    <div>
      <p>Parent: {count}</p>
      <button onClick={() => setCount(count + 1)}>Parent Count</button>
      <Child onClick={handleClick} />
    </div>
  );
}

const Child = React.memo(function Child({ onClick }) {
  console.log("子コンポーネントがレンダリングされました");
  return <button onClick={onClick}>Click</button>;
});

3. useMemo

useMemoは、計算コストの高い値の再計算を防ぐフックです。依存配列が変更されない限り、以前の計算結果を再利用します。

export default function Counter() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(10);

  const double = (count) => {
    let i = 0;
    while (i < 100000000) i++;
    return count * 2;
  };

  const doubleCount = useMemo(() => double(count2), [count2]);

  return (
    <div>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>

      <p>
        Counter: {count2}, {doubleCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
    </div>
  );
}

4. コンポーネントの分割とローカル状態の活用

状態の変更が影響する範囲を最小限に抑えるために、コンポーネントを適切に分割し、状態をできるだけローカルに保つことが重要です。

// 良くない例:すべての状態が一つのコンポーネントにある
export default function MonolithicComponent() {
  const [user, setUser] = useState({ name: "山田" });
  const [count, setCount] = useState(0);
  const [products, setProducts] = useState([]);

  // userが変更されてもproductsセクションが再レンダリングされる
  return (
    <div>
      <UserProfile user={user} onUpdate={setUser} />
      <Counter count={count} onIncrement={() => setCount(count + 1)} />
      <ProductList products={products} />
    </div>
  );
}

// 良い例:状態をコンポーネントに分離する
export default function App() {
  return (
    <div>
      <UserContainer />
      <CounterContainer />
      <ProductContainer />
    </div>
  );
}

export default function UserContainer() {
  const [user, setUser] = useState({ name: "山田" });
  return <UserProfile user={user} onUpdate={setUser} />;
}

export default function CounterContainer() {
  const [count, setCount] = useState(0);
  return <Counter count={count} onIncrement={() => setCount(count + 1)} />;
}

export default function ProductContainer() {
  const [products, setProducts] = useState([]);
  return <ProductList products={products} />;
}

まとめ

React の再レンダリングを理解し、最適化することは、高性能な React アプリケーションを構築するための重要なスキルです。以下が主なポイントです:

  1. 再レンダリングの仕組み

    • 状態が変わったとき

    • props(親から渡されるデータ)が変わったとき

    • 親コンポーネントが更新されたとき

  2. 最適化テクニック

    • React.memoで不必要な再レンダリングを防止

    • useCallbackで関数の参照を安定化

    • useMemoで計算コストの高い値を記憶

    • コンポーネントの適切な分割と状態の局所化

最適化は必要なときだけ行いましょう。最初から全部最適化しようとすると、コードが複雑になり、かえって理解しにくくなります。まずはシンプルに作り、本当に遅いと感じたら、ツール(React Developer Tools)を使って問題箇所を特定してから最適化するのがおすすめです!

シェア!

Threads
icon
三嶋雅幸
SES企業でエンジニアをしています。今後のキャリアアップのために、頑張っていきたいと思います。
Loading...
記事一覧に戻る
Threads
0