Yanonoblog!

こつこつと

useCallbackの概要と用途の解説

はじめに

本記事では「モダンJavaScriptの基本から始める React実践の教科書」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。

書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。

詳細は2023年の3月にリリースされた新しいReactの公式ドキュメントを参照してまとめています。

超基礎的な内容です!

レンダリング

Reactでは、コンポーネントの状態やプロパティが変更された場合、それに応じてコンポーネントが再度レンダリングされます。

レンダリングのトリガーとなる要因は、状態の変更やプロパティの変更だけでなく、親コンポーネントの再レンダリングコンポーネントのマウントやアンマウントなども含まれます。Reactはこれらの変更を検知し、必要な場合に再レンダリングをトリガーすることで、柔軟で効率的なUIの構築を実現しています。

レンダリングが発生する条件

Reactコンポーネントの再レンダリングパターンは3つあります。

  1. Stateが更新されたコンポーネント
  2. Propsが変更されたコンポーネント
  3. レンダリングされたコンポーネントの子コンポーネント全て

コンポーネントは特にPropsが変更されていなくても親が再レンダリングされたら再レンダリングされるため、場合によっては表示が変わらないのに無駄なサイレンダリングを行うことでパフォーマンスが低下することになってしまうため、最適化する必要があります。

React.useCallBack

useCallbackは関数のメモ化を行うことができます。

呼び出しの結果をキャッシュしておき、サイレンダリングなどの際にはキャッシュを再利用することにより再計算を防ぐという手法です。

useCallback の構文は以下の通りです。

useCallback(関数, 依存配列)

第二引数の依存配列の要素が更新されない限りは第一引数の関数を行わずに関数の算出結果のキャッシュを返します。

下記の例では、handleCountUphandleResetコールバック関数をuseCallbackでラップしています。

関数が同じインスタンスを保持し続けるため、コンポーネントの再レンダリング時はキャッシュした値が返されます。これによって、不要な再レンダリングが発生せず、パフォーマンスが向上します。

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

function Counter() {
  const [count, setCount] = useState(0);

  // カウントアップのコールバック関数
  const handleCountUp = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  // リセットのコールバック関数
  const handleReset = useCallback(() => {
    setCount(0);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleCountUp}>Count Up</button>
      <button onClick={handleReset}>Reset</button>
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <Counter />
    </div>
  );
}
注意点: 適切なタイミングで使用しないとコストがかかる

useCallbackは不要な再レンダリングを避けるために有効なアプローチですが、そもそもメモ化の処理自体が少し重いため適切な場面で使用する必要があります。

useCallbackが有効なケース

下記のコードは大量のアイテムリストをレンダリングするコンポーネントで、リストの再レンダリングを最適化するためにReact.memo()でラップされています。

import useItemSearch from './fetch-items';

const ItemList = ({ searchKeyword, onItemClick }) => {
  const items = useItemSearch(searchKeyword);

  const handleItemClick = item => {
    console.log('You clicked on item: ', item);
    onItemClick(item);
  };

  const renderItems = () => {
    return items.map(item => (
      <div onClick={() => handleItemClick(item)}>{item.name}</div>
    ));
  };

  return <div>{renderItems()}</div>;
}

export default React.memo(ItemList);

次のコードでは、onItemClick関数をuseCallbackでラップしています。

import { useCallback } from 'react';

export default const ParentComponent = ({ keyword }) => {
  const onItemClick = useCallback(item => {
    console.log('選択されたアイテム ', item);
    // 処理...
  }, [keyword]);

  return (
    <ItemList
      searchKeyword={keyword}
      onItemClick={onItemClick}
    />
  );
}

useCallbackには、itemを引数に持つ関数を指定し、依存配列としてkeywordを指定しています。

memoでカバーできないケースを補完できるのがuseCallback

React.memoを使用してコンポーネントをメモ化すると、コンポーネントのプロパティが変更されない限り、再レンダリングがスキップされます。

ただし、関数が新しいインスタンスを持っている場合、メモ化が正しく機能せず、再レンダリングが発生する可能性があります。useCallbackを使用することで、関数が同じインスタンスを維持し、メモ化の効果を最大限に活用できます。

これによりuseCallbackによって、ParentComponentが再レンダリングされても、onItemClick関数のインスタンスは変わらず、ItemListのメモ化が維持されます。

まとめ
  • useCallbackは関数のメモ化を行うフックで、キャッシュされた関数のインスタンスを再利用することで不要な再計算を防ぐことができる
  • useCallback(callback, dependencies)で依存配列を指定し、その要素が更新されない限りキャッシュを返す
  • 大量のアイテムリストをレンダリングするコンポーネントの最適化などで有効
  • React.memoでのメモ化だけでは、関数のインスタンスが変わると再レンダリングが発生する場合がある
  • useCallbackを使うことで、関数のインスタンスを維持し、メモ化の効果を最大限に活用できる
  • useCallbackを使う場面では注意が必要で、適切なタイミングで使用する必要がある

参考

続く…

コメント

本記事の内容は以上になります!

書籍の続きのアウトプットも随時更新したいと思います。


プログラミングスクールのご紹介 (卒業生より)

お世話になったプログラミングスクールであるRUNTEQです♪

https://runteq.jp/r/ohtFwbjW

こちらのリンクを経由すると1万円引きになります。

RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。

もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。

https://twitter.com/outputky