第87章:練習:useMemo で重たい計算をスキップする
1️⃣ 今日のゴール 🎯
この章では、
「めちゃくちゃ重たい計算があるけど、必要なときだけ実行したい!」
というときに使う useMemo を、実際のミニアプリを作りながら体で覚えることがゴールです✨
やることはシンプル👇
- 「カウント」を増やすボタン ➕
- 「メモ用のテキスト入力欄」📝
- カウントに応じて 重たい計算を行う関数 を用意
- まずは
useMemoなしで動かして 大変さを体感 - 次に
useMemoを入れてサクサクに改善 ✨
2️⃣ ざっくりイメージ:useMemo ってどこで効いてるの?🧐
Reactは「Stateが変わる」たびに、そのコンポーネントをもう一度よみがえらせる(再レンダリング) っていう動きをします。 このとき、コンポーネントの中で
- 超重たい計算関数(ループしまくりとか)
- フィルター・ソートしまくる処理
などがあると、ちょっとした変更でも毎回それが走ってしまうんですね😇
useMemo はざっくりいうと…
「入力が変わってないなら、前に計算した結果を使い回そう」
という、結果のメモ帳(キャッシュ) です✨
3️⃣ 図で見る:useMemo がない場合 vs ある場合 🧩
Mermaid図でイメージを整理してみます🌟
イメージとしては、
-
通常の関数:再レンダリングのたびに毎回がっつり計算
-
useMemo:- 「前と同じ入力かな?」とチェック
- 同じなら「前の結果そのまま出すね〜」とラクをする🎵
という感じです。
4️⃣ 作るミニアプリの仕様 💻✨
こんな画面を目指します👇
- 「カウント」:
+1/-1ボタン - 「メモ用テキスト」:自由に入力できる
- 「重たい計算の結果」:
countを元に計算した何かの数字 - DevToolsのコンソールで 「重たい計算が実行されたよ!」というログを出す
ポイントはここ👇
テキスト入力を打つだけで、 重たい計算が毎回走るのはもったいないよね?
これを、useMemo で解決していきます💪
5️⃣ STEP 0:準備(プロジェクトを起動)🚀
すでに Vite + React + TS プロジェクトがある前提で進みます。
-
プロジェクトフォルダを開く(例:
my-react-app) -
ターミナルで
npm install
npm run dev -
ブラウザで
http://localhost:5173を開く
OKなら、src/App.tsx を編集していきましょう🧑💻
6️⃣ STEP 1:useMemo なしバージョンを作る(あえて重くする)🔥
まずは あえてダメなやり方 を作ります。
これで、useMemo のありがたみがハッキリわかります✨
src/App.tsx を、いったんまるっと書き換えてOKです👇
import { useState, ChangeEvent } from "react";
function heavyCalculation(num: number): number {
console.log("🔥 heavyCalculation が呼ばれました!");
// 疑似的に「重たい処理」を再現する
// 回数はPCに合わせて調整してね(最初は 20_000_000 くらいが無難)
let total = 0;
for (let i = 0; i < 20_000_000; i++) {
total += i % 7;
}
// 実際の結果は num をちょっと加工しただけ
return num * 2 + (total % 1000);
}
function App() {
const [count, setCount] = useState<number>(0);
const [text, setText] = useState<string>("");
const handleIncrement = () => {
setCount((prev) => prev + 1);
};
const handleDecrement = () => {
setCount((prev) => prev - 1);
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
// ❌ useMemo なしで、毎回重たい計算を実行している
const result = heavyCalculation(count);
return (
<div style={{ padding: "24px", fontFamily: "system-ui" }}>
<h1>useMemo 練習アプリ 💡</h1>
<section style={{ marginBottom: "16px" }}>
<h2>カウント 🔢</h2>
<p>count: {count}</p>
<button onClick={handleDecrement} style={{ marginRight: "8px" }}>
-1
</button>
<button onClick={handleIncrement}>+1</button>
</section>
<section style={{ marginBottom: "16px" }}>
<h2>メモ入力 ✍️</h2>
<input
type="text"
value={text}
onChange={handleChange}
placeholder="ここにテキストを入力してね"
style={{ padding: "4px 8px", width: "280px" }}
/>
<p>入力中のテキスト:{text}</p>
</section>
<section>
<h2>重たい計算の結果 🧮</h2>
<p>結果: {result}</p>
<p style={{ fontSize: "12px", color: "#666" }}>
※ コンソールを開くと、いつ計算が走っているか見えます 👀
</p>
</section>
</div>
);
}
export default App;
7️⃣ STEP 2:一度動かして「問題」を体感する 😵💫
-
ブラウザを開いてアプリにアクセス
-
Chrome DevTools などで コンソールを表示 する
-
次の動きを試してみてください👇
+1ボタンを押す → コンソールに🔥 heavyCalculation が呼ばれました!が出るメモ入力のテキストボックスに文字を打つ → なんと、1文字打つたびに 同じログが出るはず!
テキスト入力って、カウントとは関係ないのに、 毎回「重たい計算」が走ってしまっている…😇
これが、useMemo を入れる前の「イマイチな状態」です。
8️⃣ STEP 3:useMemo を追加して「必要なときだけ」計算させる ✨
ここからが本番です💪
heavyCalculation(count) を useMemo で包む ことで、
「
countが変わったときだけ計算してね〜」
とReactに伝えてあげます。
src/App.tsx を、次のように少しだけ修正します👇
ポイントは 2つだけ:
useMemoをインポートするheavyCalculation(count)をuseMemoで包む
🔧 修正版コード(useMemo あり)
import { useState, useMemo, ChangeEvent } from "react";
function heavyCalculation(num: number): number {
console.log("🔥 heavyCalculation が呼ばれました!");
let total = 0;
for (let i = 0; i < 20_000_000; i++) {
total += i % 7;
}
return num * 2 + (total % 1000);
}
function App() {
const [count, setCount] = useState<number>(0);
const [text, setText] = useState<string>("");
const handleIncrement = () => {
setCount((prev) => prev + 1);
};
const handleDecrement = () => {
setCount((prev) => prev - 1);
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
// ✅ useMemo を使って、count が変わったときだけ
// heavyCalculation を実行するようにする
const result = useMemo(() => {
return heavyCalculation(count);
}, [count]);
// 👆👆 依存配列(この中の値が変わったら計算し直す)
return (
<div style={{ padding: "24px", fontFamily: "system-ui" }}>
<h1>useMemo 練習アプリ 💡</h1>
<section style={{ marginBottom: "16px" }}>
<h2>カウント 🔢</h2>
<p>count: {count}</p>
<button onClick={handleDecrement} style={{ marginRight: "8px" }}>
-1
</button>
<button onClick={handleIncrement}>+1</button>
</section>
<section style={{ marginBottom: "16px" }}>
<h2>メモ入力 ✍️</h2>
<input
type="text"
value={text}
onChange={handleChange}
placeholder="ここにテキストを入力してね"
style={{ padding: "4px 8px", width: "280px" }}
/>
<p>入力中のテキスト:{text}</p>
</section>
<section>
<h2>重たい計算の結果 🧮</h2>
<p>結果: {result}</p>
<p style={{ fontSize: "12px", color: "#666" }}>
※ コンソールログの回数に注目してみてね 👀
</p>
</section>
</div>
);
}
export default App;
9️⃣ STEP 4:ほんとに速くなった?確認しよう ✅
もう一度ブラウザで試してみましょう👟
+1/-1ボタンを押す → コンソールに🔥 heavyCalculation が呼ばれました!- テキスト入力欄に文字をたくさん打つ → 今度は ログが増えない はずです ✨
つまり、
countが変わる →useMemoが「入力が変わった!」と判断 → 計算するtextだけ変わる →countは変わってない → 前の結果を再利用する
という流れになっています🎉
🔍 useMemo の型の話(軽くだけ)🧬
TypeScript 的には、useMemo の戻り値にもちゃんと型が付きます。
今回の例だと、
heavyCalculationの型:(num: number) => numberuseMemoで包んだ結果:resultの型はnumber
もし、もっと厳密に書きたいときはこんな書き方もできます👇
const result = useMemo<number>(() => {
return heavyCalculation(count);
}, [count]);
でも今回のレベルなら、型推論に任せてOK です👌
🔁 ここまでのまとめメモ 📝
-
Reactは Stateが変わるたびにコンポーネントを再レンダリング する
-
その中で 重たい計算 があると、関係ないStateの変更でも毎回走ってしまう😵
-
useMemoを使うと…useMemo(() => 計算, [依存する値])- 「この 依存配列の中の値 が変わったときだけ、もう一回計算してね」
- それ以外は 前の計算結果を再利用 してくれる
-
今回の練習では
heavyCalculation(count)をuseMemoで包んでcountが変わったときだけ実行されるようにした ✨
🎓 発展チャレンジ(余裕があったら)🚀
もし「もうちょっとやってみたい!」と思ったら、こんなチャレンジもおすすめです👇
-
重たい計算の中身を変えてみる
- 例:
1〜count * 10000までの素数の個数を数える関数にしてみる 🔢
- 例:
-
countとは別の State を増やしてみる- 例:
theme(ライト / ダーク)を切り替えるボタンを作る - →
themeを切り替えてもheavyCalculationが呼ばれないことを確認 👀
- 例:
-
依存配列をわざと間違えてみる
- 例:
[], [text], [count, text]などに変えて挙動の違いをチェック - 「あ、これだと毎回計算されちゃうんだ…」と感覚がつかめます✨
- 例:
次の章では、useMemo と useCallback を「いつ使うか?」という判断基準の話に進んでいきます💡
今回の useMemo 練習アプリは、ぜひそのまま手元に残して、あとから見返せるようにしておいてくださいね〜😊💻✨