本文へスキップ
useCallback

Part 8 · パフォーマンス

44 / 64

useCallback

関数を毎回作り直さない

memo の章で、関数を props で渡すと memo が効かないと書きました。 その対処がこの章です。

useMemo が計算結果を覚えておく道具なら、useCallback関数そのものを覚えておく道具です。

memo したのに、子が描き直される

子は memo で包んであります。 渡している props は onSave という関数 1 つだけ。 そして「count を増やす」は、子とは何の関係もありません。

素の関数を渡すうまくいかない例

memo してあるのに、押すたびに子が光る

count: 0 / 保存した回数: 0

memo した子

子が光ります。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 が効くようになります。

useCallback で包む直した例

count を押しても、子は光らない

count: 0 / 保存した回数: 0

memo した子

子の描き直しが止まりました。子のコードは 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>
  );
}