[React] 関数をメモ化して同じ処理が再実行されないようにする(前編)

2021.10.02

こんにちは、CX事業本部 IoT事業部の若槻です。

今回は、Reactアプリケーションのパフォーマンスチューニングとして、関数をメモ化して同じ処理が再実行されないようにする方法を確認してみた、の前編です。(後編はこちら

再実行されてしまうパターン1

以下では同じ関数getResult()を複数箇所で呼び出しています。

import React from 'react';

const arg = 10;

export const App: React.FC = () => {
  const getResult = () => {
    console.log('getResult() executed.'); // getResult()が何回実行されたかを確認
    return arg * 10;
  };

  return (
    <>
      <div>計算結果A: {getResult() * 2}</div>
      <div>計算結果B: {getResult() / 2}</div>
    </>
  );
};

実行結果を見ると、Consoleにログが2回出力されているためgetResult()が2回実行されていることが分かります。

しかしgetResult()の実行結果は毎回同じであるため、複数回呼び出されると同じ処理が再実行されてパフォーマンス上よろしくありません。

再実行されてしまうパターン2(useCallbackだけを使う)

getResult()useCallbackを利用してメモ化してみます。これによりuseCallbackの依存配列の要素のいずれかが変化した場合にのみ関数が変わります。

import React, { useCallback } from 'react';

const arg = 10;

export const App: React.FC = () => {
  const getResult = useCallback(() => {
    console.log('getResult() executed.'); // getResult()が何回実行されたかを確認
    return arg * 10;
  }, []);

  return (
    <>
      <div>計算結果A: {getResult() * 2}</div>
      <div>計算結果B: {getResult() / 2}</div>
    </>
  );
};

しかし実行結果を見ると、Consoleにログが2回出力され、getResult()が2回実行されていました。useCallbackだけを使いメモ化するだけではだめのようです。

再実行されないパターン(useCallbackでメモ化し、useState、useEffectを使う)

正解パターンはこちらでした。

下記のようにgetResult()をuseCallbackでメモ化した上で、useEffectでgetResultが変更された時のみ実行してその結果をuseStateのステートに代入しています。

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

const arg = 10;

export const App: React.FC = () => {
  const getResult = useCallback(() => {
    console.log('getResult() executed.'); // getResult()が何回実行されたかを確認
    return arg * 10;
  }, []);

  const [result, setResult] = useState<number>(2);

  useEffect(() => {
    setResult(getResult());
  }, [getResult]);

  return (
    <>
      <div>計算結果A: {result * 2}</div>
      <div>計算結果B: {result / 2}</div>
    </>
  );
};

Consoleにログが1回だけ出力されています。getResult()を複数回呼び出すこと無く、その結果を複数回利用することができました。

補足:useCallbackを使わない場合

ちなみに上記でuseStateとuseEffectは使うがuseCallbackを使わない場合。

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

const arg = 10;

export const App: React.FC = () => {
  const getResult = () => {
    console.log('getResult() executed.');
    return arg * 10;
  };

  const [result, setResult] = useState<number>(2);

  useEffect(() => {
    setResult(getResult());
  }, [getResult]);

  return (
    <>
      <div>計算結果A: {result * 2}</div>
      <div>計算結果B: {result / 2}</div>
    </>
  );
};

Consoleにログが2回出力されており、getResult()が複数回実行されています。

またgetResult()はeslintによりwarningとなっています。

関数を副作用フックのdependencyに利用する際は、useCallbackでその関数をメモ化しないとレンダリングごとにその関数が実行されてしまうとのこと。だからuseCallback、useState、useEffectをセットで使う必要があったんですね。

The 'getResult' function makes the dependencies of useEffect Hook (at line 15) change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'getResult' in its own useCallback() Hook. (react-hooks/exhaustive-deps)eslint

正解パターンは後編です。

はじめ「useCallbackでメモ化し、useState、useEffectを使う方法」を正解パターンとしていましたが、もっと簡潔な記述でメモ化する方法を教えて頂いたので後編にて紹介します。

参考

以上