こんにちは!フロントエンド・アーキテクートの私です。
Reactを触り始めて、「あれ、なんだか画面の動きがカクつくぞ?」「親をちょっと変えただけなのに、子コンポーネントまで全員再レンダリングされてる!?」なんて壁にぶつかっていませんか?
大丈夫です、その悩み、誰もが必ず通る道ですから安心してください。
今日は、そんなReactのパフォーマンス最適化の切り札である`React.memo`と、その裏側でめちゃくちゃ重要な役割を持つ「Propsの比較」について、おしゃべりするような感覚で一緒に紐解いていきましょう!
—
1. なぜReactはすべてを再レンダリングしたがるのか?
まず、Reactの性格をちょっと知っておきましょう。
Reactって、すごく「真面目」なんです。親御さん(親コンポーネント)の機嫌(状態)がちょっとでも変わると、「家族みんなに知らせなきゃ!」と、下にいる子どもたち(子コンポーネント)全員を片っ端から呼び出しちゃうおせっかい屋さん。
身近な例えで言うと、「家族全員分の今日の晩ご飯の献立表」を想像してください。
お母さんが「あ、やっぱり今日のお味噌汁の具はワカメじゃなくて豆腐にするわ」と言っただけで、家族全員(子どもも、おじいちゃんも、ペットのポチも)に対して、「いいですか!?今日の晩ご飯はワカメではなく豆腐に変更になりました!!」と、全員分のプリントを新しく刷り直して配り歩くようなものです。
……いやいや、おじいちゃん、そんなのどうでもいいよね?おじいちゃんは自分の部屋で寝てるんだから、献立のプリントなんざ配らなくていいよ!って思いますよね。
この「おじいちゃんにはプリントを配らなくていいようにする」魔法の呪文が、`React.memo`なんです。
—
2. React.memoってなに?おじいちゃんを守る盾
`React.memo`は、子コンポーネントを包み込む「ちょっと賢いお墨付き」のようなものです。
import React from ‘react’;
// 普通の子コンポーネント
function ChildComponent({ name }) {
console.log(“子コンポーネントが再レンダリングされたよ!”);
return
こんにちは、{name}さん!
;
}
// React.memoで包むと、「おじいちゃんモード」になります
const MemoizedChild = React.memo(ChildComponent);
`React.memo`で包まれたコンポーネントは、親から渡されたProps(プロパティ)が「前回とまったく同じ」であれば、親が何度再レンダリングされても、「我関せず」と自分の再レンダリングをサボる(スキップする)ようになります。
「おや、私宛ての荷物(Props)の中身は先週から変わってないな。じゃあわざわざ着替えて玄関に出る必要ないか、寝とこ」というわけですね。省エネ、最高!
—
3. ここでつまずく!「あれ、メモしたのに再レンダリングされるぞ?」の罠
さて、ここからが現場のエンジニアが頭を抱えるリアルな罠のお話です。
「よし、`React.memo`で包んだから完璧だぜ!」と思ってアプリを動かしてみると……あれ? 親のボタンを押すたびに、子コンポーネントの `console.log` が元気に鳴り響く。ぜんぜんサボってくれない!
なんでやねん!とお思いでしょう。原因は、JavaScriptの「参照の不一致」というちょっといじわるな性質にあります。
お買い物の例えで考えてみよう
あなたがスーパーで、まったく同じ「100円のりんご」を2個、別々のカゴに入れてレジに持っていったとします。
人間から見れば「中身はどっちもりんごだし、同じでしょ」と思いますよね。
でも、Reactのデフォルトの比較(浅い比較:Shallow Equal)は、こう言います。
- 「カゴAに入っているりんごのメモリーアドレス(住所)」と、「カゴBに入っているりんごの住所」は違う! だから中身は別物だ!
そう、JavaScriptでは、オブジェクトや関数を新しく作るたびに「新しい住所(参照)」が割り振られます。親が再レンダリングされるたびに、関数やオブジェクトの「新しい住所の入れ物」が新しく作られてしまうため、Reactは「おっ、新しいデータが届いたぞ!再レンダリングしなきゃ!」と勘違いしてしまうのです。
—
4. 救世主!カスタム比較関数で「中身」を見てあげよう
「住所が違っても、中身のIDや名前が一緒ならスルーしてよ!」
そんなワガママを叶えてくれるのが、`React.memo`の第2引数に渡せる「カスタム比較関数(arePropsEqual)」です。
百聞は一見に如かず、 TypeScriptできれいに型付けされた実用的なコードを見てみましょう。
import React, { useState } from ‘react’;
// 子コンポーネントが受け取るPropsの型定義
interface UserCardProps {
user: {
id: number;
name: string;
job: string;
};
onSelect: (id: number) => void;
}
// 子コンポーネント本体
const UserCard: React.FC
console.log(`[${user.name}] のカードがレンダリングされました!`);
return (
{user.name}
職業: {user.job}
);
};
/
- 💡 ここがキモ!カスタム比較関数
- 前回のProps(prevProps)と、次回のProps(nextProps)を比較します。
- 「true」を返すと再レンダリングを「スキップ(サボる)」します!
- 「false」を返すと通常通り再レンダリングします。
/
const arePropsEqual = (
prevProps: UserCardProps,
nextProps: UserCardProps
): boolean => {
// ユーザーのIDと名前、職業が同じであれば、住所が違っても「変化なし」とみなす!
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.job === nextProps.user.job
// ※onSelect関数は毎回新しく作られることが多いので、ここであえて比較対象から外すのが現場のテクニックです
);
};
// React.memoの第2引数に、いま作った比較関数を渡します
const MemoizedUserCard = React.memo(UserCard, arePropsEqual);
// 親コンポーネント
export default function ParentApp() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ id: 1, name: ‘山田 太郎’, job: ‘フロントエンドエンジニア’ });
return (
親のカウンター: {count}
{/ 親のステートが変わるとParentApp全体が再レンダリングされます /}
{/
ユーザーのデータ自体は変わっていないので、
上のボタンを押しても MemoizedUserCard は再レンダリングをサボります!
/}
/>
);
}
コードの解説と実務の知見
このコードのミソは、`arePropsEqual` 関数の中で「何を見て、何を見ないか」を私たちが明示的にコントロールしている点です。
関数(`onSelect`)などは、親がレンダリングされるたびに新しく生まれ変わるため、厳密に比較しようとすると沼にハマります。そのため、実務では上記のように「表示に必要なプリミティブな値(IDや文字列、数値など)だけを比較して、関数やオブジェクト自体の参照比較はあえてやらない」というアプローチを取ることがよくあります。
—
5. チーフアーキテクトからの優しいアドバイス
ここまで読んで、「なるほど、全部のコンポーネントを `React.memo` で包んで、カスタム比較関数を書けば最強じゃん!」と思ったそこのあなた。
ちょっと待った!ストップです。実はこれ、初心者がやりがちな「すべてのものにメモを貼りまくる病(過剰最適化)」なんです。
`React.memo` もタダではありません。
- コンポーネントを包むことによる「前回のPropsと今回のPropsを比較する計算コスト」
- コードの複雑化(メンテナンス性の低下)
これらが毎回のレンダリング時に発生するため、「めちゃくちゃ重い処理をしているコンポーネント」や「リスト形式で大量に並ぶ子要素」以外で使うと、かえってアプリの動作が重くなる本末転倒な事態が起きます。
ですので、最初から完璧を目指さなくて大丈夫ですよ!
「あ、なんかこの画面、動きがもたつくな」と感じたボトルネックに対して、「ここに `React.memo` を貼ってみようかな」と外科手術的に処方していくのが、プロの現場でも一番スマートで愛されるやり方です。
一歩一歩、確実に行きましょう。今日のあなたのReact知識が、また一段と深まったはずです。応援しています!

コメント