🔖

【React】そろそろuseCallbackを実務で使いたい。

に公開

useCallback使ってますか?

みなさんuseCallback使っていますでしょうか?
私は恥ずかしながら実務で一回も使えていません。
というのも、なぜだかmemo化というものは、サイトのパフォーマンスを上げるためだけに必要で、中級者以上のエンジニアが使うものだという偏見があったからです。
もちろん実際にそうなのかもしれませんが、そろそろ自分も触れてみてもいい頃かなと思い、改めて深掘りしてみました。
今回はuseCallbackを調べましたので、アウトプットがてらブログに書き起こそうと思います。

useCallbackとは

useCallbackは再レンダー間で関数定義をキャッシュできるようにするReactフックです。
以下のコードは、useCallbackを使うことで、不要な再レンダリングを防ぐ効果を確認するための例です。

"use client";
import React, { useState, useCallback } from "react";

const Child = React.memo(function Child({ onClick }) {
  console.log("Child rendered");
  return <button onClick={onClick}>Click me</button>;
});

export default function Page() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log("Clicked");
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <Child onClick={handleClick} />
    </div>
  );
}

このコードでは、Pageコンポーネント内でcountという状態を管理しており、「Increment」ボタンを押すとcountが1ずつ増えていきます。
また、ChildコンポーネントにはhandleClickという関数をpropsとして渡しており、React.memoを使ってpropsが変わらない限り再レンダリングされないようにしています。
しかし、実際に「Increment」ボタンを押すと、Childコンポーネントも再レンダリングされてしまいます。
JavaScriptでは、関数やオブジェクトは毎回新しい参照として扱われるため、たとえ中身が同じでも「違うもの」と認識されます。
そのため、Pageが再レンダリングされるたびにhandleClickも新しい関数として再生成され、Childに渡るpropsも「変わった」と判断されてしまうのです。
この問題を解決するために、useCallbackを使ってhandleClick関数をメモ化(再利用可能に保持)します。

"use client";
import React, { useState, useCallback } from "react";

const Child = React.memo(function Child({ onClick }) {
  console.log("Child rendered");
  return <button onClick={onClick}>Click me</button>;
});

export default function Page() {
  const [count, setCount] = useState(0);

  // useCallbackでメモ化
  const handleClick = useCallback(() => {
    console.log("Clicked");
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <Child onClick={handleClick} />
    </div>
  );
}

上記のようにhandleClickをuseCallbackで囲むことで、依存配列が変わらない限り関数の参照が変わらなくなります。
これにより、React.memoに渡している Child コンポーネントはpropsの変化を検知せず、再レンダリングがされなくなります。
実際にこのコードで「Increment」をクリックしても「"Child rendered"」は表示されなくなりました。
依存が変わらない限り、handleClickの参照は変わらなくなるのです。
結果として、Childコンポーネントのpropsも変わらないと判断され、再レンダリングされなくなります。

ユースケース

React公式では、useCallbackが役立つケースは主に2つあるとされています。

useCallback で関数をキャッシュすることが有用なのはいくつかのケースに限られます。
・それを memo でラップされたコンポーネントに props として渡すケース。この場合は、値が変化していない場合には再レンダーをスキップしたいでしょう。メモ化することで、依存値が異なる場合にのみコンポーネントを再レンダーさせることができます。
・あなたが渡している関数が、後で何らかのフックの依存値として使用されるケース。たとえば、他の useCallback でラップされた関数がそれに依存している、または useEffect からこの関数に依存しているケースです。

1つ目のケース(memo化されたコンポーネントに関数を渡すケース)は、先ほど具体例で紹介しました。
後者のほうは、関数をuseEffectの依存配列に入れるケースです。
先ほど言及したように、JavaScriptでは関数は再定義されるたびに別物として扱われるため、useEffectの依存値に直接関数を渡すと、毎回エフェクトが発火してしまうことがあります。
これを防ぐために、useCallbackで関数をメモ化します。

const fetchData = useCallback(() => {
  // データを取得する処理
}, []);

useEffect(() => {
  fetchData(); // fetchDataが変わらない限り再実行されない
}, [fetchData]);

さいごに

今回メモ化の中でも関数をメモ化するuseCallbackについて紹介しました。
React初学者がuseCallbackを理解するには、「関数は生成されるたびに毎回違うものとして認識される」ことや「コンポーネントが再レンダリングされる条件」などを知っておく必要があるので、難しいのだなと思いました。
また、ユースケースが限られており、実務を重ねないと使いどころをつかむのも難しいと感じました。
React公式には以下のようなことも書かれており、パフォーマンスの悪いと感じるページがあったら、そこで初めてuseCallbackの必要性を考え、組み込めそうな箇所があれば組み込んでみるというノリで扱っていこうかなと僕は考えています。

あなたのアプリがこのサイトのように、ほとんどのインタラクションが大まかなもの(ページ全体やセクション全体の置き換えなど)である場合、メモ化は通常不要です。一方、あなたのアプリが描画エディタのようなもので、ほとんどのインタラクションが細かなもの(図形を移動させるなど)である場合、メモ化は非常に役に立つでしょう。

Discussion