React では、親コンポーネントが再レンダリングされると、その中の子コンポーネントも基本的にすべて再レンダリングされます。多くの場合これで問題ありませんが、重い処理を持つ子が何度も無駄に再描画されると、動作が重く感じられることがあります。そんなときに使えるのが React.memo です。この記事では、React.memo の基本、useCallback や useMemo と組み合わせる理由、そして「使いすぎない」ための判断基準まで、初心者向けに解説します。
目次
なぜ子コンポーネントが再レンダリングされるのか
React は、状態(state)が変わったコンポーネントを再レンダリングします。このとき、そのコンポーネントが持つ子コンポーネントも、props が変わっていなくても一緒に再レンダリングされるのが既定の動作です。次の例では、ボタンを押してカウントを増やすたびに、無関係な Child まで毎回描画されます。
function Child({ label }: { label: string }) {
console.log("Child を描画:", label);
return <p>{label}</p>;
}
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>count: {count}</button>
{/* label は変わらないのに、count が増えるたびに再描画される */}
<Child label="こんにちは" />
</div>
);
}
Child に渡している label は一度も変わっていません。それでも count が増えるたびに Child を描画 のログが出ます。この「変わっていないのに再描画される」ムダを省くのが React.memo です。
React.memo でメモ化する
コンポーネントを React.memo(...) で包むと、React は前回と今回の props を比較し、変わっていなければ再レンダリングをスキップします。前回の描画結果を再利用する(=メモ化する)わけです。
import { memo } from "react";
// props が変わらない限り、再レンダリングされない
const Child = memo(function Child({ label }: { label: string }) {
console.log("Child を描画:", label);
return <p>{label}</p>;
});
export default Child;
これで、先ほどの Parent で count を増やしても、label が同じなら Child は再描画されません。ログも最初の1回しか出なくなります。React.memo は、props を「前回と同じか」で判定し、同じならスキップする——というシンプルな仕組みです。
props が関数・オブジェクトだと効かない
React.memo の props 比較は「浅い比較」で、=== による同一性チェックです。文字列や数値なら値が同じなら等しいと判定されますが、関数やオブジェクト、配列は「中身が同じでも毎回新しく作られる」と別物と見なされ、メモ化が効きません。
function Parent() {
const [count, setCount] = useState(0);
// レンダリングのたびに新しい関数が作られる → memo が効かない
const handleClick = () => console.log("clicked");
return (
<div>
<button onClick={() => setCount(count + 1)}>count: {count}</button>
<Child onClick={handleClick} />
</div>
);
}
handleClick は再レンダリングのたびに作り直されるため、Child から見ると毎回「違う props」になり、React.memo をすり抜けてしまいます。これを防ぐには、関数を useCallback、オブジェクトや配列を useMemo で包み、同じ内容なら同じ参照を保つようにします。
import { useCallback, useState } from "react";
function Parent() {
const [count, setCount] = useState(0);
// 依存配列が空なので、常に同じ関数を使い回す
const handleClick = useCallback(() => console.log("clicked"), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>count: {count}</button>
<Child onClick={handleClick} /> {/* これで memo が効く */}
</div>
);
}
このように、React.memo は useCallback や useMemo とセットで使ってはじめて効果を発揮する場面が多くあります。関数やオブジェクトを props で渡すコンポーネントをメモ化するときは、この点を必ず意識しましょう。
比較の方法を自分で決める
React.memo の第2引数に比較関数を渡すと、「どんなときに同じと見なすか」を自分で決められます。関数が true を返すと「同じ=再レンダリングしない」という意味になる点に注意してください。
const UserCard = memo(
function UserCard({ user }: { user: { id: number; name: string } }) {
return <p>{user.name}</p>;
},
// id が同じなら「変わっていない」と見なして再描画しない
(prev, next) => prev.user.id === next.user.id,
);
使いすぎに注意する
React.memo は便利ですが、すべてのコンポーネントに付ければ速くなるわけではありません。props の比較そのものにもコストがかかるため、軽いコンポーネントに付けると、かえって手間が増えるだけのこともあります。目安としては、「描画に時間がかかる」「頻繁に再レンダリングされる親の中にある」「props がほとんど変わらない」——このような条件がそろったコンポーネントに絞って使うのが効果的です。まずは付けずに書き、実際に動作が重いと感じた箇所を計測してから導入する、という順序をおすすめします。
まとめ
React.memo は、props が変わっていなければ再レンダリングをスキップして、前回の結果を再利用する仕組みです。ただし関数やオブジェクトを props で渡すときは、useCallback や useMemo で参照を固定しないと効果がありません。第2引数の比較関数で判定方法をカスタマイズすることもできます。何にでも付けるのではなく、「重い・頻繁に再描画される・props が変わりにくい」コンポーネントに絞って使うのが、パフォーマンス改善の近道です。