【React】useCallbackのキホン

Categories:React

ここではReack hookのひとつ、useCallbackについて紹介していきたいと思います。

このhookはReactにおけるパフォーマンス最適化戦略と関わってくるところが大きいので、まずはそこから見ていきましょう。

Reactのパフォーマンス最適化

Reactでは、パフォーマンス最適化のためには「不要な再計算やコンポーネントの再レンダリングを抑える」ことが基本的な戦略です。

その実現のために使われるのが、React.memoやuseCallback、useMemoです。今回はuseMemo以外の2つを取り上げます。

React.memo

React.memoとは、コンポーネント(のレンダリング結果)を「メモ化(計算結果を保持し、再利用できるように)」するものです。いわゆる「短期記憶」、キャッシュですね。

このReact.memoを用いてレンダリングコストが高いコンポーネントや、頻繁に再レンダリングされるコンポーネント内のサブコンポーネントのリレンダーをスキップすることで、パフォーマンス向上が見込めます。

React.memoの例

import React from 'react';

export default function App() {
  const Hello = React.memo(({ name }) => {
    return <h1>Hello, React</h1>;
  });

  return (
    <div className="App">
      <Hello />
    </div>
  );
}

React.memoはPropsの等価性をチェックし、再レンダリングするか判断をします。つまり、新たに渡されたPropsと保持していたPropsとを比較し、等しい値であればメモ化したコンポーネントを再利用する、という処理を内部で行なっています。上のサンプルコードで言うと、props.nameが更新されない限りはコンポーネントの再レンダリングは実施されません。

しかし、このメモ化の仕組みで困ったことが起こります。コールバック関数をPropsとして受け取った場合、等価性を満たせず常に再レンダリングが走ってしまうのです。関数の内容が同じでも、その参照が異なるからです。

// コールバック関数を受け取るせいで常に再レンダリングが走ってしまうコード

import React, { useState } from "react";

const Child = React.memo(({ handleClick }) => {
  console.log("I am Child.");
  return <button onClick={handleClick}>Child</button>;
});

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

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

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

この問題を解消するためのhookが、useCallbackです。

useCallback

useCallbackは、先に見たReact.memoにおける問題を避けるため、メモ化したコールバック関数を返すhookです。React.memoでメモ化したコンポーネントに対し、useCallbackでメモ化したコールバック関数をPropsとして渡すことで、コンポーネントが必要以上にレンダリングされることを防ぎます。関数の参照を等価に保つhook、ということですね。

useCallbackの例と注意点

以下は、先に挙げた「コールバック関数を受け取るせいで常に再レンダリングが走ってしまうコード」をuseCallbackで書き換えた例です。

import "./styles.css";
import React, { useCallback, useState } from "react";

const Child = React.memo(({ handleClick }) => {
  console.log("I am Child.");
  return <button onClick={handleClick}>Child</button>;
});

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

  // useCallbackでメモ化して再レンダリング抑制
  const handleClick = useCallback(() => {
    console.log("clicked");
  }, []);

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

注意点として、コールバック関数をメモ化するuseCallbackはあくまでもコンポーネントをメモ化するReact.memoと併用するものなので、以下のように使っても本来期待する再レンダリング抑制の効果は得られないことが挙げられます。

  • React.memoでメモ化していないコンポーネントに対して、useCallbackでメモ化した関数を渡す
  • useCallbackでメモ化した関数を、その生成元のコンポーネント自身で利用する

参考

返信がありません

コメントを残す

メールアドレスが公開されることはありません。