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

2021.10.04

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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

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

再実行されないパターン(useMemoでメモ化)

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

React hooksのuseMemoを使用することにより、useCallbackを使う場合よりも記述をさらに簡潔にすることができました。

import React, { useMemo } from 'react';

const arg = 10;

export const App: React.FC = () => {
  const result = useMemo(() => {
    console.log('getResult() executed.'); // getResult()が何回実行されたかを確認
    return arg * 10; // 実際はもっと重い処理を実施
  }, []);

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

Consoleにログが1回だけ出力されています。計算処理を複数回呼び出すこと無く、その結果だけを使い回すことができています。

useCallbackはメモ化された関数を返しますが、useMemoはメモ化された値を返します。この計算処理の結果であるメモ化された値を使用することにより、計算処理を複数回行うこと無く、計算結果だけを使い回すことができます。

メモ化された値を返します。

“作成用” 関数とそれが依存する値の配列を渡してください。useMemo は依存配列の要素のいずれかが変化した場合にのみメモ化された値を再計算します。この最適化によりレンダー毎に高価な計算が実行されるのを避けることができます。

実例

さてここまでは四則演算(arg * 10)のようなメモ化による高速化も必要ないような単純な計算処理をサンプルコードとしてきましたが、実際には別途外部からAPIで取得した何百行もあるObjectのListに対してMapなどの処理をガリガリ行うような重たい計算処理を想定しています。

下記は、Axiosによるデータ取得処理が別途あり、その取得結果に対する計算処理の結果をuseMemoでメモ化しています。

import React, { useMemo, useEffect, useState } from 'react';
import Axios from 'axios';

interface Issue {
  state: 'open' | 'close';
}

const USER = 'octocat';
const REPO = 'hello-world';
const PER_PAGE = 100;

export const App: React.FC = () => {
  //データ取得(GitHub公式の公開リポジトリのIssueのList)
  const [data, setData] = useState<Issue[]>([]);
  useEffect(() => {
    console.log('fetching data...');
    Axios.get(
      `https://api.github.com/repos/${USER}/${REPO}/issues?state=all&per_page=${PER_PAGE}`,
      {
        headers: {
          accept: 'application/vnd.github.v3+json'
        }
      }
    ).then((res) => {
      setData(res.data);
    });
  }, []);

  //データの計算結果の取得
  const result = useMemo(() => {
    const length = data.length;
    if (length) {
      //計算処理
      console.log('getResult() calculation executed.');
      const openedIssuesCount = data.filter(
        (d) => d.state === 'open'
      ).length;
      return {
        openedIssuesCount: openedIssuesCount,
        closedIssuesCount: length - openedIssuesCount
      };
    }
    //初回レンダリング時(データ取得未完了時)の戻り値
    return { openedIssuesCount: 0, closedIssuesCount: 0 };
  }, [data]);

  return (
    <>
      <h3>
        リポジトリ "{USER}/{REPO}" Issue数内訳
      </h3>
      <div>オープン中: {result.openedIssuesCount}</div>
      <div>クローズ済み: {result.closedIssuesCount}</div>
    </>
  );
};

Consoleにログが1回だけ出力されています。

ちなみに動作としてはuseMemo内の処理は初回描画時とデータ取得完了時の2回実施されています。dataがdependencyであるためです。しかし初回描画時は計算処理を実施しないようにしているため、結果として計算処理が行われたのは1回のみとなっています。

参考

以上