Reactでのレンダリング制御の基本:useCallback・useMemo・React.memoの使い方と挙動理解
投稿日: 2025年11月12日
なんか急にレンダリングの制御の挙動を理解してみたいなっていうのをふと思ったので、
実証も兼ねてuseCallback・useMemo・React.memoあたりの基本的な使い方と、どんな挙動になるのかをみていきたいと思います!
※間違っていたら、コメントいただけると幸いです笑
あと、内容が長めなので途中で読むのやめたくなる可能性があります笑
まずは、useCallback・useMemo・React.memoそれぞれどんな特徴でどんな時に使うのかを基本的な内容になりますが紹介していきます!🗿🗿🗿🗿🗿🗿
関数処理にあてるレンダリング制御として使う。
関数をメモ化(記憶)することで、不要なレンダリングを制御する。
親側にある関数処理にuseCallbackを使い、子がその関数を受け取りReact.memoを使って、レンダリングを制御する。
カスタムフックによる重い計算処理、オブジェクトまたは配列での計算処理に活用する。
例:filterやmapによる配列処理で、毎回実行するのを防ぐ。
中身が同じでも、毎回新しいオブジェクトや配列の参照を固定するのに使う。
React.memoを使った子コンポーネントにオブジェクトや配列に渡す際、propsが変わっていないことを正しく認識させるため。
受け取ったpropsや関数をレンダリング制御に活用する。
例えば複数のPropsを親から子コンポーネントが受け取った場合、前回渡された時と全てが同一のもの(インスタンス)であれば、
親が再レンダリングされても、Propsを受け取った子コンポーネントの再レンダリングはスキップされます。
逆に、複数のpropsの中で一つでも再レンダリングによって、前回と内容が違ったらコンポーネント全体の再レンダリングが行われるってことです。
複数のPropsを親から子コンポーネントが受け取り、前回渡された時と全てが同一だった場合
props.count が前回と 同一 (===)
props.text が前回と 同一 (===)
props.onCountClick が前回と 同一 (===)
全てのPropsが 同一 (===) だった
結果: 「全く変更なし」と判断し、再レンダリングをスキップする
逆に、複数のpropsの中で1つでも再レンダリングによって、前回と内容が違ったらコンポーネント全体の再レンダリングが行われた場合
props.count が前回と 同一 (===)
props.text が前回と 不一致(!==) (ここで不一致を発見!)
props.onCountClick はもう比較さえしない
結果: 「変更あり」と判断し、コンポーネント全体を再レンダリングする。
以下の対策はあくまでケースバイケースが前提となります。
Props毎にレンダリング制御するなら、コンポーネントを小さく分割、それぞれの子コンポーネントが受け取るPropsを最小限にし、React.memoでレンダリング制御する。
// 親コンポーネント
"use client";
import { useState, useCallback } from "react";
import { CountComponent } from "./component/button/CountComponent";
import "./globals.css";
const MyComponent = () => {
const [count, setCount] = useState(0);
// ★ 効果を実証するための子に「無関係な」State
const [text, setText] = useState("");
console.log("親コンポーネントが再レンダリング");
// 2. この関数は子コンポーネントに渡される
const handleCountClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-10 bg-white text-black font-bold">
<div>
<label>親のState(子には無関係): {text}</label>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
className="border p-2"
/>
</div>
{/* React.memo された子に渡すからこそ useCallback が活きる */}
<div className="">
<CountComponent count={count} onCountClick={handleCountClick} />
</div>
</div>
);
};
export default MyComponent;// 子コンポーネント
"use client";
import React from "react";
import "../../globals.css";
type CountProps = {
count: number;
onCountClick: () => void;
};
const ChildComponent = ({ count, onCountClick }: CountProps) => {
// このログが出るかどうかが鍵
console.log("--- 子コンポーネントが再レンダリングされました ---");
return (
<div>
<p className="mb-10 text-center text-2xl">{count}</p>
<button
type="button"
className="border-3 rounded-2xl py-2 px-4 bg-blue-400 text-2xl"
onClick={onCountClick}
>
カウントボタン🗿
</button>
</div>
);
};
export const CountComponent = React.memo(ChildComponent);パターン1:「カウントボタン🗿」を押す
count が変わります。
親が再レンダリングされます。
子の count Propsが変わったので、子は再レンダリングされます。
コンソールログ:
親コンポーネントが再レンダリング
--- 子コンポーネントが再レンダリングされました ---パターン2:「親のState(子には無関係)なテキスト」入力欄に文字を入力する(ここが最重要!)
text Stateが変わり、親コンポーネントだけが再レンダリングされます。
console.log("親...") が出力されます。
React.memo が子のPropsを比較します。
count: 変わっていません。
onCountClick: useCallback のおかげで変わっていません(=前回と同一のインスタンス)。
React.memo は「全てのPropsが変わっていない」と判断し、子の再レンダリングをスキップします
子は、textpropsを受け取っていなのでtextデータを知りません。なので、 「全てのPropsが変わっていない」と判断。
コンソールログ:
親コンポーネントが再レンダリング
(★子のログは出力されない!)// 親コンポーネント
"use client";
import { useState } from "react";
import { CountComponent } from "./component/button/CountComponent";
import "./globals.css";
const MyComponent = () => {
const [count, setCount] = useState(0);
// ★ 効果を実証するための「無関係な」State
const [text, setText] = useState("");
console.log("親コンポーネントが再レンダリング");
// 2. この関数は子コンポーネントに渡される
const handleCountClick = () => {
setCount((prev) => prev + 1);
};
return (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-10 bg-white text-black font-bold">
<div>
<label>親のState(子には無関係): {text}</label>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
className="border p-2"
/>
</div>
<div className="">
<CountComponent count={count} onCountClick={handleCountClick} />
</div>
</div>
);
};
export default MyComponent;// 子コンポーネント
"use client";
import "../../globals.css";
type CountProps = {
count: number;
onCountClick: () => void;
};
export const ChildComponent = ({ count, onCountClick }: CountProps) => {
// このログが出るかどうかが鍵
console.log("--- 子コンポーネントが再レンダリングされました ---");
return (
<div>
<p className="mb-10 text-center text-2xl">{count}</p>
<button
type="button"
className="border-3 rounded-2xl py-2 px-4 bg-blue-400 text-2xl"
onClick={onCountClick}
>
カウントボタン🗿
</button>
</div>
);
};パターン1:「カウントボタン🗿」を押す
count が変わります。
親コンポーネント(MyComponent)が再レンダリングされます。
子コンポーネント(CountComponent)に渡される count Propsが変わったので、子は(正しく)再レンダリングされます。
コンソールログ:
親コンポーネントが再レンダリング
--- 子コンポーネントが再レンダリングされました ---
(※このパターンは、【ある】場合と全く同じ結果になります)
パターン2:「親のState(子には無関係)なテキスト」入力欄に文字を入力する(ここが最重要!)
text Stateが変わり、親コンポーネント(MyComponent)だけが再レンダリング。
console.log("親...") が出力。
親の再レンダリングに伴い、handleCountClick 関数が新しく生成(useCallback がないため)
子コンポーネント(CountComponent)は React.memo で守られていません。
Reactのデフォルトのルール(親が再レンダリングしたら、子も再レンダリングする)が適用。
子は、count Propsが変わっていなくても、親に**"つられて"無条件に再レンダリング**される。
コンソールログ:
親コンポーネントが再レンダリング
--- 子コンポーネントが再レンダリングされました --- <-- ★不要な再レンダリングが発生!
結論: 「無関係なtext」を操作しただけで、CountComponent のログが出力されてしまうこと(=不要な再レンダリング)が確認できます。
これは、React.memoがいないため、親の再レンダリングがそのまま子に伝播してしまうためです。
パターンA:両方【ない】(React.memo も useCallback もない)
子(React.memo なし): 比較機能がありません。
結果: 親が再レンダリングされたので、子は無条件に再レンダリングされます。
パターンB:React.memo【だけ】ある
親(useCallback なし): handleCountClick が新しい関数として生成されます。
子(React.memo あり): Propsを比較します。
count: 変わってない (OK)
onCountClick: 新しい関数が来た! (NG)
結果: 「Propsが変わった」と誤認し、再レンダリングされます。
パターンC:useCallback【だけ】ある
親(useCallback あり): handleCountClick は前回と同一の関数が準備されます。
子(React.memo なし): 比較機能がありません。
結果: 比較されず、親につられて無条件に再レンダリングされます。
パターンD:両方【ある】(理想)
親(useCallback あり): handleCountClick は前回と同一の関数が準備されます。
子(React.memo あり): Propsを比較します。
count: 変わってない (OK)
onCountClick: 前回と同一の関数が来た! (OK)
結果: 「全てのPropsが変わっていない」と正しく判断し、再レンダリングをスキップします!