はじめに
React一年目の自分が習った知識を実務で活かすための備忘録です。純粋なレンダリングを追いたいため、strictModeは削除しています。
目次
- useCallback, useMemo, memo の使用方法とその役割
- memo
- useCallback
- useMemo
- メモ化まとめ
- 座学での知識を実務で活かすために必要な観点について考えてみた
- メモ化しない判断について
useCallback, useMemo, memo の使用方法とその役割
memo
memoはコンポーネントのメモ化を担当します。具体的に動作を確認しましょう。
下記のコードはボタン二つを提供する画面です。

import { useState } from "react";
const Count = ({text, count}) => {
console.log(`text is ${text} and count is ${count}`)
return <div>{`text is ${text} and count is ${count}`}</div>
}
export default function App() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
return (
<>
<Count count={countA} text={"test1"}></Count>
<Count count={countB} text={"test2"}></Count>
<button onClick={() => {setCountA(prev => prev + 1)}}> Aボタン </button>
<button onClick={() => {setCountB(prev => prev + 1)}}> Bボタン </button>
</>
)
}
画面表示した際、それぞれのコンポーネントの処理はどうなるでしょうか。コードを追いましょう。
Appが上から読み込まれ、Count二つとbutton二つが画面に表示されます。Countコンポーネントにはconsole.logがあるため、二つログが出そうですね。正解は、、、

それぞれログ出力されていますね。次に、どちらかのボタンを一回クリックしたらどうなるか考えてみましょう。Reactはstateの変更を検知すると、そのstateがあるコンポーネントを上から再実行します。
Aボタンをクリックしたとします。setCountが実行され、countAのstateが更新されます。これをReactは検知し、Appコンポーネントを上から再実行し始めます。先ほどと同様に、二つのCountと二つのbuttonがレンダリングされます。ログはもう二つ出そうですね。正解は、、、

やはりログが二つ出ましたね。動作は正しいですが、改善の余地があります。Bのボタンは見た目も中身も変わっていないのに、レンダリングされてしまっています。理想の動作としては、変化があったAボタンのみ再レンダリングされてほしいですよね。これを解決するのがmemoです。コンポーネントをまるっとmemoで囲むだけで使用できます。囲むことで、propsの変化があった場合にのみ再レンダリングされるようになります。
import { memo, useState } from "react";
const Count = memo(({text, count}) => {
console.log(`text is ${text} and count is ${count}`);
return <div>{`text is ${text} and count is ${count}`}</div>;
});
export default function App() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
return (
<>
<Count count={countA} text={"testA"}></Count>
<Count count={countB} text={"testB"}></Count>
<button onClick={() => {setCountA(prev => prev + 1)}}> Aボタン </button>
<button onClick={() => {setCountB(prev => prev + 1)}}> Bボタン </button>
</>
);
}
memoでまるっと囲んでみました。この状態でAボタンを押すと、、、

testBについてのログが出ていませんね。成功です。これで不要なtestBのレンダリングを防ぐことができました。
今回はtestBのレンダリングがあってもなくても動作に違いは感じませんでしたが、このCountコンポーネントが激重で、表示に20秒かかってしまう場合、余計に20秒待たせてしまいます。大事ですね。
useCallback
さて、先ほどのmemoを使用した例ではpropsに変化がないコンポーネントを再レンダリングから除外することができましたね。先ほどのpropsはcount、つまり変数でした。関数の場合はどうなるでしょうか。一段ずつ検証していきます。
先ほどのコードの中でpropsとして関数を渡していたのはbuttonでしたね。このbuttonがいつレンダリングされるのか確認するため、Buttonコンポーネントを作成しちゃいましょう。
import { memo, useState } from "react";
const Count = memo(({text, count}) => {
console.log(`text is ${text} and count is ${count}`);
return <div>{`text is ${text} and count is ${count}`}</div>;
});
const Button = memo(({onClick, buttonName}) => {
console.log(`${buttonName} is rendered`);
return (
<button onClick={onClick}>{buttonName}</button>
);
});
export default function App() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
return (
<>
<Count count={countA} text={"testA"}></Count>
<Count count={countB} text={"testB"}></Count>
<Button onClick={() => {setCountA(prev => prev + 1)}} buttonName="Aボタン" />
<Button onClick={() => {setCountB(prev => prev + 1)}} buttonName="Bボタン" />
</>
);
}
さて、ボタンをクリックした際、ログはどうなるでしょうか。Buttonに渡すpropsはonClickとbuttonNameです。この二つはstateではありません。先ほどのようにmemoで囲っているので、propsが変わっていないため再レンダリングされないはずです。結果は、、、

どちらもレンダリングされてしまいました。要因はpropsのonClickが関数であることです。実は関数は再生成されるため、レンダリング前の() => {setCountA(prev => prev + 1)}と、レンダリング後の() => {setCountA(prev => prev + 1)}は同じではないとReactが検知し、Buttonが再レンダリングされます。これを解決するのがuseCallbackです。useCallbackで関数をメモ化することで、レンダリング前とレンダリング後で関数が同じであることをReactに知らせることができます。渡す関数が同じということを知らせられれば、コンポーネントは再レンダリングされません。ちなみに、JavaScriptの仕組みとして、数値や文字列などのプリミティブ型は、値が同じなら「同じもの」として比較されます。しかし、オブジェクト(関数も含む)は、定義されるたびに新しいメモリ空間に保存されるため、見た目が同じ関数でも「別物(新しい参照)」として扱われてしまいます。
import { memo, useCallback, useState } from "react";
const Count = memo(({text, count}) => {
console.log(`text is ${text} and count is ${count}`);
return <div>{`text is ${text} and count is ${count}`}</div>;
});
const Button = memo(({onClick, buttonName}) => {
console.log(`${buttonName} is rendered`);
return (
<button onClick={onClick}>{buttonName}</button>
);
});
export default function App() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
const handleClickA = useCallback(() => {
setCountA(prev => prev + 1);
}, []);
const handleClickB = useCallback(() => {
setCountB(prev => prev + 1);
}, []);
return (
<>
<Count count={countA} text={"testA"}></Count>
<Count count={countB} text={"testB"}></Count>
<Button onClick={handleClickA} buttonName="Aボタン" />
<Button onClick={handleClickB} buttonName="Bボタン" />
</>
);
}
ログからも、ボタンのレンダリングが防がれていることが分かりますね

これで、変化がないカウントも、ボタンも再レンダリングを防ぐことができました!
今回のhandleClickは常にstateに1を加算するものでしたが、stateに変数を加算するものだった場合、変数によって関数が再生成されないと誤った動作をしてしまいます。

import { memo, useCallback, useState } from "react";
const Count = memo(({text, count}) => {
console.log(`text is ${text} and count is ${count}`);
return <div>{`text is ${text} and count is ${count}`}</div>;
});
const Button = memo(({onClick, buttonName}) => {
console.log(`${buttonName} is rendered`);
return (
<button onClick={onClick}>{buttonName}</button>
);
});
export default function App() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
const [step, setStep] = useState(1);
// 依存配列が空[] - 関数は一度だけ作成され、常に同じインスタンス
const handleClickA = useCallback(() => {
setCountA(prev => prev + 1);
}, []);
// 依存配列にstepを含む - stepが変わると新しい関数が作成される
const handleClickB = useCallback(() => {
setCountB(prev => prev + step);
}, [step]);
return (
<>
<Count count={countA} text={"testA"}></Count>
<Count count={countB} text={"testB"}></Count>
<Button onClick={handleClickA} buttonName="Aボタン" />
<Button onClick={handleClickB} buttonName="Bボタン" />
<div>
<label>Step: </label>
<input
value={step}
onChange={(e) => setStep(Number(e.target.value))}
/>
</div>
</>
);
}
Aボタンをクリックすると今まで通り元の値に1を加算しますが、Bボタンをクリックすると元の値に画面下部で入力した数字の値を加算します。handleClickBにあるuseCallbackの第二引数にstepを指定することで、stepが変更された際に関数の再生成がされます。ここで、inputの値を変化させたときに何がレンダリングされるか考えてみましょう。ログはどうなるでしょうか。結果は、、、

Bボタンが何度もレンダリングされましたね。memoで見た通り、Appの上からコンポーネントがreturnされていく中で、reactが変化のあるpropsを探し、handleClickBのみ再生成されたためBボタンが再レンダリングされた、というわけですね。memoはコンポーネントのメモ化、useCallbackは関数の再生成を防ぐためのものという理解ができましたね。
useMemo
useMemoは関数の結果をメモ化するものです。関数内の処理で依存する変数に変化がない場合、関数を実行した結果は同じなのでパフォーマンスの向上が期待できます。具体的には、下記のheavyFnは処理に必要なcountAの値が変わる時にのみ実行されます。countBの値が変化した時は、重い処理は走りません。なお、useMemoは関数の戻り値を返すため、利用側ではheavyFnという変数として扱えます。
import { memo, useCallback, useMemo, useState } from "react";
const Count = memo(({text, count}) => {
console.log(`text is ${text} and count is ${count}`);
return <div>{`text is ${text} and count is ${count}`}</div>;
});
const Button = memo(({onClick, buttonName}) => {
console.log(`${buttonName} is rendered`);
return (
<button onClick={onClick}>{buttonName}</button>
);
});
export default function App() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
const heavyFn = useMemo(() => {
console.log("--- 重い計算(1億回のループ)を開始します ---");
let i = 0;
while (i < 100000000) i++; // 擬似的な重い処理
return countA * 100;
}, [countA]);
return (
<>
<Count count={countA} text={"testA"}></Count>
<Count count={countB} text={"testB"}></Count>
<Button onClick={() => setCountA(prev => prev + 1)} buttonName="Aボタン" />
<Button onClick={() => setCountB(prev => prev + 1)} buttonName="Bボタン" />
</>
);
}
メモ化まとめ
- memoはコンポーネントのメモ化、コンポーネントのpropsとして関数を渡す場合はmemoを使用しても再レンダリングされてしまうため、それを防ぐために関数をメモ化するuseCallback、これらとは若干レイヤーが異なる(と感じた)重たい関数の処理を、関数内の変数が変わらない限り再実行せず結果をメモ化してくれるuseMemoの三点でした。
座学での知識を実務で活かすために必要な観点について考えてみた
座学と実践は少々異なると思ったため、既存コードの使用状況から分かることをまとめました。
- 重い処理はループをぶん回すだけではなく、外部ライブラリとの連携や読み込み中に表示するスケルトン画面など、様々なものに当てはまるため、関数を作成する際には処理がどれくらい重いか考える必要がある
- useEffect内で関数を生成する際は、useCallbackでメモ化しないとuseEffectが何度も実行されてしまうため、useCallbackを使用する一例としてはuseEffect内である
- フック内での関数をuseCallbackでメモ化しないと、フックの呼び出し元で毎回関数が再生成されてしまい、不要なメモリ圧迫やuseEffectが絡んで無限ループを引き起こす可能性がある。フック内で関数の処理をするときはuseCallbackを使用し、値を返すときはuseMemoを使用する。
- URLによって計算結果を出す場合、URLを依存配列にしてuseMemoを使用することで、URLが変わったときのみ計算しなおすという Single Source of Truth を守ることができる
座学での知識と、上記のこういう時はこうする、というものを頭に入れて実務に取り組むことで、さらに発展して「こういう時もメモ化したらよいのではないか」などを即座に思いつけるようになりたいです。
個人的に、カスタムフックを作る時は必須の知識だと感じました。とりあえず、今はこれらを守ってコードを書くことに集中したいと思います。
メモ化しない判断について
さいごに、メモ化しない判断についても考えてみます。関数をなんでもかんでもuseMemo化するのはダメみたいです。メモ化はメモをするのでメモリ消費をします(当たり前か)し、関数が一度生成、実行されて使われない場合にもメモリ上に存在するためパフォーマンスが低下します。なんでもかんでもメモ化し、可読性とパフォーマンスを低下させないようにしようと思いました。