こんにちは!Reactの学習、楽しく進められていますか?
画面のボタンを押したら数字が増えたり、文字がパッと切り替わったりする仕組みが作れるようになると、「おっ、自分、すごいアプリを作ってるぞ!」ってワクワクしてきますよね。
でも、少しずつ複雑な画面を作っていくと、こんな不思議な現象に出くわすことはありませんか?
「親のボタンを押して数字を『1』変えただけなのに、なぜかまったく関係ないはずの子どもパーツまで、ぜんぶ再レンダリング(再描画)されている……!」
これ、Reactを学び始めた人が確実に一度はハマる「あるある」の落とし穴なんです。大丈夫、あなたのコードがおかしいわけではありません。Reactの優しさが、時にはお節介になってしまっているだけなんです。
今回は、この「不要な再レンダリング」というモヤモヤをスッキリ解消する魔法のコンビ、`React.memo` と `useCallback` の使い方を、身近な例え話と一緒に優しく紐解いていきましょう!
—
なぜ「不要な再レンダリング」が起きるの?お買い物に例えてみよう
まずは、Reactが裏側でどう動いているのかを、身近な「お買い物」に例えて考えてみましょう。
あなたがお店で、レジの店員さん(親コンポーネント)と、横にいるお会計の金額を表示する小さな電卓(子コンポーネント)をイメージしてください。
今、カゴの中にある「りんごの数(状態)」を `1` から `2` に変えたとします。
店員さんは「りんごの数が変わったから、電卓の表示を更新しなきゃ!」と、電卓に向かってこう言います。
「ねえ電卓くん、りんごの数が変わったから、もう一回ぜんぶ計算し直して画面を描き直して!」
たとえ、電卓の近くにある「レシート立て」のメモ書き(propsとして渡していない情報)が1ミリも変わっていなかったとしても、店員さん(親)が動き出すと、電卓(子)は「はーい!」と言って、関係ない部分まで律儀に一から再計算(再描画)してしまうのです。
これが、Reactの基本ルール。「親が再レンダリングされると、子どもも問答無用で再レンダリングされる」 という仕組みです。
子コンポーネントがすごく複雑で、重たい処理を抱えていたらどうでしょう? カゴにりんごを入れるたびに画面がカクついたり、動作が重くなったりしてしまいますよね。それは困ります。
—
そこで登場するのが「React.memo」という防音ガラス
「親が動いても、関係ない子どもまで巻き込まないでよ!」
そんな私たちの願いを叶えてくれるのが、`React.memo` です。
イメージとしては、親と子どもの間に「頑丈な防音ガラス」を一枚挟むようなもの。
`React.memo` で包まれた子コンポーネントは、こう判断します。
「親御さん、あなたが動いたみたいだけど……私に渡されている『材料(props)』の内容は、さっきと一言一句変わっていないよね? だったら、わざわざ描き直す必要ないよね。お昼寝してていいですか?」
この賢い仕組みのおかげで、親が何度再レンダリングされても、渡された材料が変わっていなければ、子どもは省エネモード(再レンダリングをスキップ)でじっと待機していてくれるようになります。
でも、ちょっと待って!「落とし穴」があります
「やった!じゃあ何でもかんでも `React.memo` で包めば最強じゃん!」と思ったそこのあなた。
実は、ここからが一番つまずきやすい、Reactのちょっといじらしいポイントなんです。
`React.memo` を使って防音ガラスを置いたのに、親が再レンダリングされるたびに、子どもがなぜか毎回起きてしまう現象が起きることがあります。
犯人は、親から子どもへ渡している「関数」の存在です。
JavaScriptの世界では、親が再レンダリングされるたびに、その中で作られている関数は「見た目は全く同じだけど、新しく生まれ変わった別のコピー(別人)」として扱われてしまいます。
防音ガラスの向こうにいる子どもは、こう言います。
「あれ? 親御さんからさっきと『同じ内容のメモ』をもらったはずだけど……なんだかインクの匂いが新しくて、さっきの人とは違う気がする!念のため描き直そう!」
この「毎回別人の関数が作られてしまう問題」を解決するのが、相棒の `useCallback` です。
—
実際のコードで動きを見てみましょう!
百聞は一見に如かず。実際に動くコードを見てみましょう。
今回は、「カウンターの数字(親)」と「文字を入力するフォーム(子)」がある画面をイメージしてください。
import React, { useState, useCallback } from ‘react’;
// ==========================================
// 子コンポーネント:ボタン
// React.memoで包んで「防音ガラス」を設置しています
// ==========================================
const ChildButton = React.memo(({ onClick, label }) => {
console.log(`【子レンダリング】: ${label} が描画されました!`);
return (
);
});
// ==========================================
// 親コンポーネント
// ==========================================
export default function ParentApp() {
const [count, setCount] = useState(0);
const [text, setText] = useState(”);
// 1. 普通の関数(親が再レンダリングされるたびに新しく生まれ変わる)
// const handleClick = () => {
// console.log(‘ボタンが押されました’);
// };
// 2. useCallbackを使ったメモ化された関数
// 「依存配列([])」が空っぽの間は、親が何度生まれ変わっても同じ関数を使い回します!
const handleClick = useCallback(() => {
console.log(‘メモ化されたボタンが押されました’);
}, []);
return (
React.memo と useCallback の連携デモ
{/ カウンター(親のstateを変更する) /}
親のカウント: {count}
{/ テキスト入力(親の別のstateを変更する) /}
placeholder=”ここになにか入力してね”
/>
いま入力した文字: {text}
{/ 子コンポーネントを配置 /}
);
}
コードの動きを解説するよ
1. 文字を入力してみる (`text` の変更)
inputに文字を打ち込むと、親コンポーネントが再レンダリングされます。
しかし、`ChildButton` に渡している `onClick` 関数は `useCallback` で守られており、さらに `ChildButton` 自体も `React.memo` で包まれているため、コンソールを見ると `【子レンダリング】` のログは流れません!(=無駄な再レンダリングを防げている!)
2. もし `useCallback` を外したらどうなる?
もし `useCallback` を使わずに関数を定義し直すと、文字を1文字入力するたびに親が再レンダリングされ、関数が「別人」扱いになり、防音ガラスを突き抜けて `ChildButton` まで毎回再レンダリングされてしまいます。
—
最後に:完璧主義になりすぎなくて大丈夫です!
ここまで「不要な再レンダリングを防ぐ方法」として熱く語ってきましたが、実務の現場で先輩エンジニアやチーフアーキテクトからよく言われる大切な本音を最後にこっそりお伝えします。
「すべてのコンポーネントに `React.memo` を貼る必要は、ぶっちゃけありません」
なぜなら、Reactの描画エンジンはそもそも非常に高速です。中身がペラペラの軽いコンポーネントを `React.memo` で包むほうが、かえって「毎回前回のプロップスと変更がないか比較する計算コスト」のほうが重くなってしまい、逆効果になることすらあるんです。
だからこそ、次のようなシーンに出会ったときに、そっとこのコンビを思い出してください。
- 「うわ、この子コンポーネント、めちゃくちゃ重い処理をしてるぞ……」
- 「リスト一覧の項目がたくさんあって、1つの操作で全部がカクつく……」
そんな「本当にパフォーマンスの改善が必要なボトルネック」に対して、`React.memo` と `useCallback` という職人の道具をスッと取り出す。それができれば、あなたもう立派なReact使いです!
最初につまずくポイントを一つずつクリアしながら、一緒に心地よいサクサク動くWebアプリを作っていきましょうね。応援しています!

コメント