【React】useCallbackの使い方を分かりやすく解説!

この記事ではReactフックの1つである『useCallback』について、

  • useCallbackとは
  • useCallbackの構文
  • useCallbackの使い方
    • useCallbackの依存配列を指定する場合の使い方
    • useCallbackの依存配列を指定しない場合の使い方
    • useCallbackを用いて再レンダリングを防ぐ
    • useEffect内でuseCallbackを用いてエフェクトが頻繁に発火するのを防ぐ

などをサンプルコードを用いて分かりやすく説明するように心掛けています。ご参考になれば幸いです。

useCallbackとは

useCallbackは、コールバック関数のメモ化を可能にするReactフックの一つです。

メモ化はキャッシュ化のことです。useCallbackの第2引数に渡す依存配列の要素が変化しない場合には、メモ化(キャッシュ化)したコールバック関数を返します。依存配列の要素のいずれかが変化した場合には、コールバック関数を再生成します。

補足

  • useCallbackはReact16.8で導入されたフックです。
  • useCallbackを使用することで、無駄な再レンダリングを防ぐことができます。

useCallbackの構文

useCallbackの構文を以下に示します。useCallbackは2つの引数を取ります。第1引数はメモ化したいコールバック関数、第2引数は依存配列です。依存配列に含まれる値が変化した場合のみ、コールバック関数が再生成されます。

useCallbackの構文

const cachedFn = useCallback(callbackFn, [dependencies])
  • callbackFn
    • メモ化したいコールバック関数
  • dependencies
    • 依存配列

useCallback使用しない場合使用した場合のコードを比較してみましょう。

useCallbackを使用しない場合のコード

const noCachedFn = () => {
  sampleFn(a, b)
}

上記のコードは一般的なアロー関数です。このnoCachedFnは、レンダリングされる度に新しく生成されます。

useCallbackを使用した場合のコード

const cachedFn = useCallback(() => {
  sampleFn(a, b)
}, [a, b]);

上記に示すようにusecallbackを使うと、依存配列の要素a,bのいずれかが変化した場合のみ、cachedFnを再生成します。一方、依存配列の要素a,bが変化しなければ、メモ化したcachedFnを再利用するため、レンダリングされる度にcachedFnを新しく生成しません。

useCallbackの使い方

useCallbackについて、以下に示している使い方をこれから説明します。

  • useCallbackの依存配列を指定する場合の使い方
  • useCallbackの依存配列を指定しない場合の使い方
  • useCallbackを用いて再レンダリングを防ぐ
  • useEffect内でuseCallbackを用いてエフェクトが頻繁に発火するのを防ぐ

useCallbackの依存配列を指定する場合の使い方

useCallbackの第2引数に依存配列を指定した場合のサンプルコードを以下に示しています。

import React, { useState, useCallback } from 'react';

const App = () => {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + step);
  }, [step]); // stepが依存配列に含まれている

  return (
    <>
      <p>Count: {count}</p>
      <input type="number" value={step} onChange={(e) => setStep(Number(e.target.value))} />
      <button onClick={increment}>Increment</button>
    </>
  );
};

export default App;

上記のサンプルコードでは、increment関数がuseCallbackを使用してメモ化されています。useCallbackの依存配列にstepが含まれているため、stepが変更されるたびにincrement関数が再生成されます。これにより、increment関数内で常に最新のstepの値が使用されます。

ポイント

  • stepの値が変更されるたびにincrement関数が再生成されるため、最新のstepの値を使用することができる。
  • useCallbackを正しく使用することで、関数の再生成を制御できる(stepが変更されないとincrement関数が再生成されない)。

useCallbackの依存配列を指定しない場合の使い方

useCallbackの第2引数に依存配列を指定しない場合のサンプルコードを以下に示しています。

import React, { useState, useCallback } from 'react';

const App = () => {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1); // stepの初期値は1

  // increment関数をuseCallbackを使用してメモ化している
  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + step);
  }, []); // 依存配列が空

  return (
    <>
      <p>Count: {count}</p>
      <input type="number" value={step} onChange={(e) => setStep(Number(e.target.value))} />
      <button onClick={increment}>Increment</button>
    </>
  );
};

export default App;

上記のサンプルコードでも、increment関数はuseCallbackを使用してメモ化されています。しかし、useCallbackの依存配列が空のため、stepの値が変更されてもincrement関数は再生成されません。その結果、incrementCount内で使用されるstepの値は最新のものになりません。初期値の1の値のままになります。

テキストボックスの数値を変えると、stepの値は変更されますが、incrementCountは再生成されず、メモ化したものを使用するため、incrementCount内で使用されるstepの値は初期値の1のままということです。

ポイント

  • useCallbackの依存配列が空のため、increment関数は初回レンダリング時に一度だけ生成され、それ以降は再生成されない。
  • stepの値を変更しても、increment関数は再生成されないため、最新のstepの値を反映することができない。

useCallbackを用いて再レンダリングを防ぐ

次に、useCallbackを使用して無駄な再レンダリングを防ぐ例を示します。

import React, { useState, useCallback } from 'react';

const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Increment</button>;
});

const App = () => {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1); // stepの初期値は1
  const [otherState, setOtherState] = useState(false);

  // increment関数をuseCallbackを使用してメモ化している
  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + step);
  }, [step]); // stepが依存配列に含まれている

  return (
    <>
      <p>Count: {count}</p>
      <input type="number" value={step} onChange={(e) => setStep(Number(e.target.value))} />
      <ChildComponent onClick={increment} />
      <button onClick={() => setOtherState(!otherState)}>Toggle Other State</button>
    </>
  );
};

export default App;

上記のサンプルコードでは、ChildComponentは親コンポーネントからuseCallbackを使用してメモ化されたincrement関数を受け取ります。Appコンポーネントが再レンダリングされた際に、メモ化したincrement関数が再生成されていなければ、ChildComponentは再レンダリングされません。

テキストボックスの数値を変えて、stepの値を変更した場合には、increment関数が再生成されるので、ChildComponentは再レンダリングされ、ChildComponent renderedとログ出力されます。

ボタンをクリックして、Appコンポーネントの他の状態(otherState)を変更しても、メモ化したincrement関数が再生成されないため(依存配列にotherStateを用いていないため再生成されない)、ChildComponentは再レンダリングされません。

ポイント

  • useCallbackReact.memoと組み合わせて使うことでパフォーマンス最適化がしやすくなります。
    • 上記のサンプルコードでは、React.memoを使用してChildComponentの再レンダリングを最適化しています。

React.memoとは

React.memoは、関数コンポーネントのメモ化を可能にするReactフックの一つです。親コンポーネントから同じプロップスを渡された場合、再レンダリングされることを防ぎます。

useCallbackを用いないことで再レンダリングされてしまう例

次に、useCallbackを用いないことで再レンダリングが発生してしまう例を示します。

import React, { useState, useCallback } from 'react';

const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Increment</button>;
});

const App = () => {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1); // stepの初期値は1
  const [otherState, setOtherState] = useState(false);

  // increment関数をメモ化していない場合
  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <>
      <p>Count: {count}</p>
      <input type="number" value={step} onChange={(e) => setStep(Number(e.target.value))} />
      <ChildComponent onClick={increment} />
      <button onClick={() => setOtherState(!otherState)}>Toggle Other State</button>
    </>
  );
};

export default App;

上記のサンプルコードでは、ChildComponentは親コンポーネントからメモ化されていないincrement関数を受け取っています。そのため、Appコンポーネントが再レンダリングされた場合、increment関数も再生成され、その結果、ChildComponentもレンダリングされてしまいます。

useEffect内でuseCallbackを用いてエフェクトが頻繁に発火するのを防ぐ

次に、useCallbackを使用して、useEffect内で実行する関数をメモ化する例を示します。これにより、レンダリングの度にエフェクトが発火するのを防ぐことができます。

import React, { useState, useEffect, useCallback } from 'react';

const App = () => {
  const [message, setMessage] = useState('');
  const [otherState, setOtherState] = useState(false);

  const outputLog = useCallback((value) => {
    console.log(value);
  },[message]);

  // Appコンポーネントの他の状態(otherState)を変更しても、Effectが発火しない
  useEffect(() => {
    outputLog(message);
  }, [outputLog]);

  return (
    <>
      <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} />
      <button onClick={() => setOtherState(!otherState)}>Toggle Other State</button>
    </>
  );
};

export default App;

上記のサンプルコードでは、outputLog関数をuseCallbackでメモ化し、それをuseEffect内で使用しています。これにより、outputLog関数は再生成されない場合、Effectの発火を防ぐことができます。

テキストボックスに入力する文字を変え、messageの値を変更した場合には、outputLog関数が再生成されるので、Effectが発火します。

一方、ボタンをクリックして、Appコンポーネントの他の状態(otherState)を変更しても、メモ化したoutputLog関数が再生成されないため(依存配列にotherStateを用いていないため再生成されない)、Effectは発火しません。

本記事のまとめ

この記事ではReactフックの1つである『useCallback』について、以下の内容を説明しました。

  • useCallbackとは
  • useCallbackの構文
  • useCallbackの使い方
    • useCallbackの依存配列を指定する場合の使い方
    • useCallbackの依存配列を指定しない場合の使い方
    • useCallbackを用いて再レンダリングを防ぐ
    • useEffect内でuseCallbackを用いてエフェクトが頻繁に発火するのを防ぐ

お読み頂きありがとうございました。