こんにちは、かつコーチです。
前回は「useMemoで無駄な再計算を防ぐ」を解説しました。
今回はそのペアとも言える useCallback を扱います。
「関数もメモ化できるの?」と思う人も多いと思いますが、useMemo の考え方が分かっていれば理解はそれほど難しくありません。
useCallbackとは?
関数そのものをメモ化する
useCallback は、関数の再生成を防ぐためのフックです。
Reactのコンポーネントは再レンダリングのたびに関数全体が再実行されるため、コンポーネント内で定義した関数も毎回新しく作り直されます。
function SearchBox() {
// レンダリングのたびに、毎回新しい関数が作られている
const handleSearch = (keyword: string) => {
console.log("検索:", keyword);
};
return <input onChange={(e) => handleSearch(e.target.value)} />;
}
見た目上は同じ処理内容でも、JavaScriptの世界では「レンダリングのたびに作られた別々の関数」として扱われます。
useCallback を使うと、依存配列の中身が変わらない限り、前回と同じ関数(同じ参照)を使い回せます。
import { useCallback, useState } from "react";
function SearchBox() {
const [count, setCount] = useState(0);
const handleSearch = useCallback((keyword: string) => {
console.log("検索:", keyword);
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>再レンダリング用: {count}</button>
<input onChange={(e) => handleSearch(e.target.value)} />
</div>
);
}
依存配列が [](空配列)なので、count が変わって再レンダリングされても handleSearch は同じ関数のまま保たれます。
useMemoとの関係
実は useCallback(fn, deps) は、useMemo(() => fn, deps) とほぼ同じ意味です。
useMemo が「計算結果」をメモ化するのに対して、useCallback は「関数そのもの」をメモ化する、という役割分担だと考えると整理しやすいです。
なぜ関数の再生成が問題になるのか
memoでラップした子コンポーネントが効かなくなる
関数の再生成が本当に問題になるのは、その関数を memo でラップした子コンポーネントにpropsとして渡しているときです。
import { memo, useState } from "react";
const SearchButton = memo(function SearchButton({
onSearch,
}: {
onSearch: () => void;
}) {
console.log("SearchButtonが再レンダリングされました");
return <button onClick={onSearch}>検索</button>;
});
function SearchScreen() {
const [count, setCount] = useState(0);
const [keyword, setKeyword] = useState("");
// レンダリングのたびに新しい関数が作られてしまう
const handleSearch = () => {
console.log("検索:", keyword);
};
return (
<div>
<button onClick={() => setCount(count + 1)}>カウント: {count}</button>
<SearchButton onSearch={handleSearch} />
</div>
);
}
SearchButton は memo でラップされているので、本来はpropsが変わらない限り再レンダリングされないはずです。
ところが handleSearch はレンダリングのたびに新しく作られる関数なので、memo から見ると「propsが変わった」と判定され、毎回再レンダリングが発生してしまいます。
つまずきやすいポイント:useCallbackを付けたのに効果が出ない
かつコーチが実際にハマった例
私が最初に useCallback を使ったとき、「付ければ何でも速くなる」と思い込んで、あちこちに機械的に追加していた時期がありました。
❌ Before:memo化していないコンポーネントにuseCallbackを使う
import { useCallback, useState } from "react";
// memoでラップされていない、普通の関数コンポーネント
function SearchButton({ onSearch }: { onSearch: () => void }) {
return <button onClick={onSearch}>検索</button>;
}
function SearchScreen() {
const [keyword, setKeyword] = useState("");
const handleSearch = useCallback(() => {
console.log("検索:", keyword);
}, [keyword]);
return <SearchButton onSearch={handleSearch} />;
}
SearchButton が memo でラップされていないため、親が再レンダリングされれば SearchButton もどのみち再レンダリングされます。
つまり、handleSearch を useCallback でメモ化しても、SearchButton 側の再レンダリングは1回も防げていない、ということに後から気づきました。
useCallback を付けること自体にも依存配列の比較コストがかかるため、効果のない場所に付けると純粋にコードが複雑になるだけの結果になります。
✅ After:memo化した子コンポーネントとセットで使う
import { memo, useCallback, useState } from "react";
const SearchButton = memo(function SearchButton({
onSearch,
}: {
onSearch: () => void;
}) {
return <button onClick={onSearch}>検索</button>;
});
function SearchScreen() {
const [keyword, setKeyword] = useState("");
const handleSearch = useCallback(() => {
console.log("検索:", keyword);
}, [keyword]);
return <SearchButton onSearch={handleSearch} />;
}
子コンポーネントを memo でラップして初めて、useCallback による関数の参照安定化が意味を持つようになります。
「memo化した子コンポーネントに渡す関数にだけuseCallbackを使う」、これが基本方針だと理解してからは、無駄な最適化に時間を使わなくなりました。
応用:カスタムフックの戻り値を安定させる
useEffectの依存配列で無限ループを防ぐ
useCallback は、カスタムフックが返す関数を安定させ、useEffect の依存配列に含めても無限ループが起きないようにする場面でも活躍します。
import { useCallback, useEffect, useState } from "react";
function useSearch(initialKeyword: string) {
const [keyword, setKeyword] = useState(initialKeyword);
const search = useCallback(async () => {
console.log("検索実行:", keyword);
}, [keyword]);
return { keyword, setKeyword, search };
}
function SearchPage() {
const { keyword, setKeyword, search } = useSearch("");
useEffect(() => {
search();
}, [search]);
return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />;
}
search を useCallback でメモ化していないと、SearchPage が再レンダリングされるたびに新しい search 関数が作られ、それを依存配列に含めた useEffect が毎回発火し続ける無限ループに陥ります。
カスタムフックから関数を返すときは、useCallback でメモ化しておくのが定石です。
まとめ
この記事のポイント
useCallbackは関数そのものをレンダリングをまたいでメモ化するフック- 効果を発揮するのは、
memoでラップした子コンポーネントに関数を渡すとき - memo化していないコンポーネントに使っても、再レンダリングは防げない
- カスタムフックが返す関数をメモ化すると、useEffectの無限ループ防止にも役立つ
次に読むべき記事
次回は「カスタムフックを自作してロジックを再利用する」です。
今回登場したuseSearchのようなカスタムフックの作り方を、体系的に解説していきます。
→ 次の記事:カスタムフックを自作してロジックを再利用する