再レンダリング入門

Reactの再レンダリングと画面更新、React.memo、useMemo、useCallbackの関係

再レンダリングは、Reactの画面更新やパフォーマンス最適化を理解するうえで避けて通れないテーマです。

学習を進めると、React.memo、useMemo、useCallback、Virtual DOM、パフォーマンス最適化といった言葉をよく見かけます。しかし、これらを理解する前に、まず「なぜコンポーネントがもう一度実行されるのか」を押さえる必要があります。

結論から言うと、再レンダリングとはコンポーネント関数がもう一度実行されることです。画面全体を毎回すべて描き直すことではありません。

そこでこの記事では、Reactの画面更新の仕組み、再レンダリングが起きるタイミング、React.memo・useMemo・useCallbackの使いどころ、実務で優先すべき状態設計を初心者向けに解説します。

結論:再レンダリングとはコンポーネント関数の再実行

まず、再レンダリングとは「Reactがコンポーネント関数をもう一度呼び出すこと」です。

function Counter() {
  console.log("Counter rendered");

  return <h1>Hello</h1>;
}

この関数がもう一度実行されると、console.logももう一度表示されます。つまり、コンソールに何度もログが出るのは、コンポーネント関数が再実行されているからです。

再レンダリング
=
コンポーネント関数の再実行

一方で、これは「ブラウザ上のDOMを全部作り直す」という意味ではありません。Reactは必要な差分を計算し、必要な部分だけをDOMへ反映します。

再レンダリングとReactの画面更新は3ステップで考える

React公式ドキュメントでは、画面更新を大きく3つのステップで説明しています。

ステップ内容初心者向けの理解
Trigger更新のきっかけが発生するstate更新などで画面更新が予約される
Renderコンポーネントを呼び出す関数を実行して新しいJSXを作る
CommitDOMへ反映する差分がある部分だけ画面に反映する

詳しい流れを確認したい場合は、React公式のRender and Commitが参考になります。なお、公式ドキュメントでも、レンダーはコンポーネントを呼び出して表示内容を計算する工程として説明されています。

初回レンダリングと再レンダリングの違い

初回表示では、Reactはルートコンポーネントから順番にコンポーネントを呼び出し、画面に必要な要素を作ります。

function App() {
  return <h1>Hello React</h1>;
}
App実行
↓
JSXを作る
↓
DOMへ反映
↓
画面に表示

更新時の再レンダリングの流れ

次に、stateが変わった場合を考えます。Reactはもう一度コンポーネントを呼び出し、新しい表示内容を計算します。

state変更
↓
コンポーネントを再実行
↓
前回の結果と比較
↓
必要な差分だけDOMへ反映

つまり、関数が再実行されても、実際のDOM更新は差分がある場所に絞られます。そのため、再レンダリングとDOMの全更新は同じではありません。

再レンダリングとVirtual DOMの関係

Virtual DOMとは、画面に表示する内容をReactが内部で扱いやすい形にしたデータ構造です。

<h1>Hello</h1>

例えば上のJSXは、イメージとしては以下のようなオブジェクトとして扱われます。

{
  type: "h1",
  props: {
    children: "Hello"
  }
}

ただし、初心者が最初に覚えるべきことは、Virtual DOMの細かい実装ではありません。重要なのは、Reactが前回と今回の表示内容を比べ、必要な変更だけを画面へ反映するという考え方です。

再レンダリングはいつ起きる?

ここでは、実務でよく見る発生タイミングを整理します。

stateが変わった時の再レンダリング

最も基本的なのは、stateが変わった時です。

const [count, setCount] = useState(0);

setCount(1);

stateが変わると、Reactは新しいUIを計算する必要があります。そのため、対象のコンポーネントがもう一度呼び出されます。

propsが変わった時の再レンダリング

親から渡されるpropsが変わった時も、子コンポーネントは更新対象になります。

<Child count={count} />

例えばcountが変わると、Childに渡されるpropsも変わります。その結果、Childも新しい表示内容を計算します。

親コンポーネント更新時の再レンダリング

実務で特に重要なのは、親が更新されると、その配下の子コンポーネントも呼び出される可能性がある点です。

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

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        +
      </button>
      <Child />
    </>
  );
}

Childにpropsがなくても、Parentが再実行されるとChildも呼び出されます。したがって、親に大きなstateを置きすぎると、更新範囲が広がりやすくなります。

再レンダリングでconsole.logが何回も出る理由

React初心者が混乱しやすいのが、console.logが何度も表示される現象です。

function Child() {
  console.log("Child");

  return <div>Child</div>;
}

これは、Child関数がもう一度実行されているためです。つまり、console.logは「DOMが全部更新された証拠」ではなく、「関数が呼び出された証拠」と考える方が正確です。

なお、開発環境のStrict Modeでは、問題を見つけるために関数が追加で呼び出されることがあります。そのため、開発中のログ回数だけで本番の挙動を判断しないようにしましょう。

再レンダリングを抑えるReact.memoとは?

React.memoは、propsが変わっていない場合にコンポーネントの再実行をスキップするための仕組みです。

公式ドキュメントでも、memoはpropsが変わらない場合にコンポーネントの再実行をスキップできるAPIとして説明されています。ただし、すべてのコンポーネントに機械的に付ければよいわけではありません。

React.memoなしの例

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

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        +
      </button>
      <Child name="田中" />
    </>
  );
}

function Child({ name }) {
  console.log("Child");

  return <p>{name}</p>;
}

countが変わるたびにParentが更新され、Childも呼び出されます。nameが変わっていなくても、親の更新に巻き込まれます。

React.memoありの例

const Child = React.memo(function Child({ name }) {
  console.log("Child");

  return <p>{name}</p>;
});

この場合、nameが前回と同じならChildの再実行をスキップできます。つまり、React.memoは「propsが同じならもう一度計算しなくてよい」とReactに伝える仕組みです。

React.memoでも再レンダリングを抑えにくい代表例

一方で、React.memoはpropsの比較がうまくいかないと効果を発揮しません。特にオブジェクト、配列、関数を毎回新しく作って渡す場合は注意が必要です。

propsよくあるコードなぜ効きにくいか
オブジェクトuser={{ ...user }}毎回新しい参照になる
配列tags={[...tags]}毎回新しい配列になる
関数onClick={() => select(id)}毎回新しい関数になる

見た目が同じ値でも、JavaScriptでは参照が違えば別物として扱われます。そのため、React.memoを使っているのに効果が出ない場合は、propsとして渡している値の作り方を確認しましょう。

useCallbackとは?関数を再利用するHook

useCallbackは、関数を再利用するためのHookです。主に、React.memoした子コンポーネントへ関数をpropsで渡す時に役立ちます。

公式のuseCallbackリファレンスでも、再レンダー間で関数定義をキャッシュするHookとして説明されています。

関数をその場で作る例

<UserItem
  onSelect={() => console.log(id)}
/>

この書き方では、親が更新されるたびに新しい関数が作られます。その結果、子コンポーネントから見るとpropsが変わったように見えます。

useCallbackを使う例

const handleSelect = useCallback((userId) => {
  console.log(userId);
}, []);

<UserItem
  user={user}
  onSelect={handleSelect}
/>

このように親側で関数を安定させると、React.memoした子コンポーネントのprops比較が通りやすくなります。ただし、依存配列を間違えると古い値を参照するため、必要な依存値は正しく入れます。

useMemoとは?計算結果を再利用するHook

useMemoは、計算結果を再利用するためのHookです。関数を再利用するuseCallbackに対して、useMemoは値を再利用します。

React公式のuseMemoリファレンスでも、再レンダー間で計算結果をキャッシュするHookとして説明されています。

毎回計算する例

const total = sales.reduce(
  (sum, sale) => sum + sale.price,
  0
);

salesが大量にあり、計算が重い場合は、更新のたびに時間がかかる可能性があります。

useMemoを使う例

const total = useMemo(() => {
  return sales.reduce(
    (sum, sale) => sum + sale.price,
    0
  );
}, [sales]);

salesが変わらない限り、前回の計算結果を再利用できます。一方で、簡単な文字列結合や少ない配列の計算までuseMemoで囲む必要はありません。

再レンダリング最適化より先に状態設計を見直す

実務では、React.memoやuseMemoを追加する前に、状態設計を見直す方が効果的なことが多いです。

例えば、モーダルの開閉状態をApp全体で持つと、モーダルを開くだけでHeaderやSidebarまで更新対象になる可能性があります。

悪い例:stateが上位にありすぎる

function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <Header />
      <Sidebar />
      <Modal />
    </>
  );
}

この場合、モーダルのためのstateがAppにあるため、更新範囲が広がりやすくなります。

良い例:stateを使う場所に近づける

function ModalButton() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        開く
      </button>
      {isOpen && <Modal />}
    </>
  );
}

状態を使う場所に近づけると、更新範囲を自然に小さくできます。つまり、最適化Hookを追加する前に、stateをどこに置くかを考えることが重要です。

再レンダリング最適化が必要な場面

普通の入力フォームや小さな画面では、最初から最適化を入れすぎる必要はありません。むしろ、コードが読みにくくなることがあります。

一方で、以下のような場面では最適化を検討する価値があります。

場面検討すること
大量リスト商品1000件、ユーザー1000件React.memo、仮想リスト
管理画面大量テーブル、検索結果一覧state分割、memo化
グラフChart.js、Recharts重い計算のuseMemo
複雑なUIドラッグ&ドロップ、動画編集UI更新範囲の分離

なお、重いかどうかは感覚だけで判断しない方が安全です。必要に応じてReact公式のProfilerやブラウザのPerformanceツールで測定しましょう。

再レンダリングの過度な最適化がよくない理由

React.memo、useMemo、useCallbackは便利ですが、過度に使うとコードが読みにくくなります。

簡単な計算まで囲む必要はない

const fullName = `${firstName} ${lastName}`;

この程度の計算なら、そのまま書く方が読みやすいです。したがって、何でもuseMemoで囲む必要はありません。

メモ化にもコストがある

メモ化は無料ではありません。Reactは依存配列を比較し、キャッシュを管理します。そのため、軽い処理に対して過剰に使うと、メリットより読みづらさの方が大きくなることがあります。

本当の設計問題を隠すことがある

また、重さの原因が状態設計にある場合、memo化だけでは根本解決になりません。stateの位置、propsの渡し方、コンポーネント分割を見直す方が効果的なことがあります。

再レンダリング対策の実務判断基準

実務では、以下の順番で考えると失敗しにくいです。

  1. まず普通に書く
  2. 重いと感じたら測定する
  3. stateの位置を見直す
  4. propsの渡し方を整理する
  5. 必要ならReact.memoを使う
  6. 関数propsが原因ならuseCallbackを使う
  7. 重い計算が原因ならuseMemoを使う

つまり、最初から最適化Hookを並べるのではなく、原因を測ってから必要な対策を選ぶことが重要です。

よくある質問

再レンダリングは悪いことですか?

悪いことではありません。Reactはstateやpropsの変化に応じてUIを更新するため、自然に再レンダリングします。問題になるのは、不要に広い範囲が何度も重く更新される場合です。

React.memoは毎回使うべきですか?

毎回使う必要はありません。propsが安定していて、子コンポーネントが重い場合や大量リストで効果が出やすいです。一方で、軽いコンポーネントに無理に使うとコードが読みにくくなります。

useMemoとuseCallbackの違いは何ですか?

useMemoは計算結果の値を再利用します。useCallbackは関数そのものを再利用します。重い計算にはuseMemo、子へ渡す関数propsを安定させたい場合はuseCallbackを検討します。

最初に見るべきポイントは何ですか?

まずはstateの位置です。状態を必要以上に上位コンポーネントへ置くと、更新範囲が広がります。したがって、stateを使う場所に近づけるだけで改善することがあります。

まとめ:Reactの最適化は仕組みを理解してから考える

再レンダリングとは、コンポーネント関数がもう一度実行されることです。ただし、画面全体のDOMを毎回すべて更新することではありません。

Reactは、state変更、render、commitの流れで新しい表示内容を計算し、必要な差分だけをDOMへ反映します。そのため、まずはこの仕組みを理解することが重要です。

React.memo、useCallback、useMemoは便利な道具ですが、魔法ではありません。実務では、状態設計、propsの渡し方、コンポーネント分割を見直したうえで、必要な場所にだけ使うのが現実的です。

React・Next.jsの実務スキルを伸ばしたい方は、まず再レンダリングの仕組みから整理してみてください。画面更新の考え方がわかると、パフォーマンス改善も判断しやすくなります。

IaC INP PM PMO PMP UX Webディレクター インフラエンジニア キャリアチェンジ フロントエンドエンジニア