Part 8 · パフォーマンス
44 / 64 章
useCallback
関数を毎回作り直さない
memo の章で、関数を props で渡すと memo が効かないと書きました。 その対処がこの章です。
useMemo が計算結果を覚えておく道具なら、useCallback は関数そのものを覚えておく道具です。
memo したのに、子が描き直される
子は memo で包んであります。 渡している props は onSave という関数 1 つだけ。 そして「count を増やす」は、子とは何の関係もありません。
memo してあるのに、押すたびに子が光る
count: 0 / 保存した回数: 0
子が光ります。memo が効いていません。
犯人はこの行です。
const handleSave = () => setSaved((s) => s + 1);memo の章で予告したとおりです。 この行は描き直されるたびに実行され、毎回新しい関数を作ります。 だから memo は「props が変わった」と判断してしまいます。
ここで、「新しい関数」とは何なのかを はっきりさせておきます。ここが分かれば、 この Part の残りは全部同じ話です。
without-callback.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useState } from "react";
// memo で包んである。props が変わらなければスキップされるはず
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
return (
<RenderBox title="memo した子">
<Button size="sm" variant="outline" onClick={onSave}>
保存
</Button>
</RenderBox>
);
});
export function WithoutCallback() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [saved, setSaved] = useState(0);
// 描き直されるたびに、新しい関数が作られる
const handleSave = () => setSaved((s) => s + 1);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">
count: {count} / 保存した回数: {saved}
</p>
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(子には関係ない)
</Button>
<Child onSave={handleSave} />
</div>
);
}同じ関数を使い回す
// 変更前
const handleSave = () => setSaved((s) => s + 1);
// 変更後
const handleSave = useCallback(() => setSaved((s) => s + 1), []);依存配列が空なので、この関数は最初に作られたものがずっと使い回されます。 毎回同じものが渡るので、memo が効くようになります。
count を押しても、子は光らない
count: 0 / 保存した回数: 0
子の描き直しが止まりました。子のコードは 1 文字も変えていません。変えたのは、渡す側の 1 行だけです。
with-callback.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useCallback, useState } from "react";
// 子はさっきとまったく同じ
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
return (
<RenderBox title="memo した子" tone="highlight">
<Button size="sm" variant="outline" onClick={onSave}>
保存
</Button>
</RenderBox>
);
});
export function WithCallback() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [saved, setSaved] = useState(0);
// 同じ関数を使い回す。依存配列が空なので、作り直されることがない
const handleSave = useCallback(() => setSaved((s) => s + 1), []);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">
count: {count} / 保存した回数: {saved}
</p>
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(子には関係ない)
</Button>
<Child onSave={handleSave} />
</div>
);
}単独では意味がない
ここが最も誤解されるところです。
具体的には、次の 2 つの場合です。
- memo した子に渡すとき(この章でやったこと)
- useEffect の依存配列に入れるとき(無限ループの章でやったこと)
逆に言えば、この 2 つに当てはまらないなら要りません。 ただのボタンの onClick を包む必要はありません。
// 包む必要なし。誰も「同じかどうか」を見ていない
<button onClick={useCallback(() => setOpen(true), [])}>開く</button>
// これで十分
<button onClick={() => setOpen(true)}>開く</button>with-callback.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useCallback, useState } from "react";
// 子はさっきとまったく同じ
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
return (
<RenderBox title="memo した子" tone="highlight">
<Button size="sm" variant="outline" onClick={onSave}>
保存
</Button>
</RenderBox>
);
});
export function WithCallback() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [saved, setSaved] = useState(0);
// 同じ関数を使い回す。依存配列が空なので、作り直されることがない
const handleSave = useCallback(() => setSaved((s) => s + 1), []);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">
count: {count} / 保存した回数: {saved}
</p>
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(子には関係ない)
</Button>
<Child onSave={handleSave} />
</div>
);
}useMemo との関係
実は useCallback は、useMemo の特別な形にすぎません。
// この 2 つは同じ意味
const fn = useCallback(() => doSomething(), []);
const fn = useMemo(() => () => doSomething(), []);「関数を返す useMemo」を書くのが面倒なので、 専用の名前が用意されているだけです。覚えることは増えていません。
with-callback.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useCallback, useState } from "react";
// 子はさっきとまったく同じ
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
return (
<RenderBox title="memo した子" tone="highlight">
<Button size="sm" variant="outline" onClick={onSave}>
保存
</Button>
</RenderBox>
);
});
export function WithCallback() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [saved, setSaved] = useState(0);
// 同じ関数を使い回す。依存配列が空なので、作り直されることがない
const handleSave = useCallback(() => setSaved((s) => s + 1), []);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">
count: {count} / 保存した回数: {saved}
</p>
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(子には関係ない)
</Button>
<Child onSave={handleSave} />
</div>
);
}理解できたか確かめる
確認クイズ
memo した子に関数を渡すと、memo が効かなくなるのはなぜ?
確認クイズ
ボタンの onClick に渡す関数を useCallback で包むべき?
without-callback.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useState } from "react";
// memo で包んである。props が変わらなければスキップされるはず
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
return (
<RenderBox title="memo した子">
<Button size="sm" variant="outline" onClick={onSave}>
保存
</Button>
</RenderBox>
);
});
export function WithoutCallback() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [saved, setSaved] = useState(0);
// 描き直されるたびに、新しい関数が作られる
const handleSave = () => setSaved((s) => s + 1);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">
count: {count} / 保存した回数: {saved}
</p>
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(子には関係ない)
</Button>
<Child onSave={handleSave} />
</div>
);
}この章のまとめ
- 関数は描き直しのたびに新しく作られる
- そのため、
memoした子に関数を渡すと memo が効かない useCallbackは同じ関数を使い回すための道具- 単独では意味がない。memo した子に渡すときと、effect の依存配列に入れるときだけ
- 正体は「関数を返す
useMemo」の短縮形
with-callback.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { memo, useCallback, useState } from "react";
// 子はさっきとまったく同じ
const Child = memo(function Child({ onSave }: { onSave: () => void }) {
return (
<RenderBox title="memo した子" tone="highlight">
<Button size="sm" variant="outline" onClick={onSave}>
保存
</Button>
</RenderBox>
);
});
export function WithCallback() {
useTrackDemoRender();
const [count, setCount] = useState(0);
const [saved, setSaved] = useState(0);
// 同じ関数を使い回す。依存配列が空なので、作り直されることがない
const handleSave = useCallback(() => setSaved((s) => s + 1), []);
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3 font-mono">
count: {count} / 保存した回数: {saved}
</p>
<Button size="sm" onClick={() => setCount((c) => c + 1)}>
count を増やす(子には関係ない)
</Button>
<Child onSave={handleSave} />
</div>
);
}