React 17と18でメモ化を自動化する方法

Reactのメモ化とは

コンポーネントや関数の「計算結果」を記憶して、再利用する仕組みのことです。

コンポーネントのメモ化には「memo」、関数のメモ化には「useCallback, useMemo」が使用されます。

※ Reactを使用したことがある人なら知らない人はいないので、メモ化の詳しい説明は割愛します。

Reactのメモ化は手動でやると結構面倒な作業です。

しかも、メモ化は「memo, useCallback, useMemo」を追加することでコードの可読性を低下させます。

例えば以下のようなコードがあったとします。

App.tsx
// メモ化していないコード📝
import { useState } from 'react'

// 重い計算をする関数
const expensiveCalculation = (num: number) => {
  let result = 0
  for (let i = 0; i < 1000000000; i++) {
    result += num * i
  }
  return result
}

// 重たい計算を行うコンポーネント
const ExpensiveComponent = ({ num }: { num: number }) => {
  const result = expensiveCalculation(num)
  
  return <div>計算結果: {result}</div>
}

const App = () => {
  const [count, setCount] = useState(0)
  const [num, setNum] = useState(5)
  const handleClick = () => {
    setCount(count + 1)
  }
  const doubledNum = num * 2

  return (
    <div>
      <h1>Reactメモ化なしのサンプル</h1>
      <button onClick={handleClick}>カウント: {count}</button>
      <p>数値の2倍: {doubledNum}</p>
      <input
        type="number"
        value={num}
        onChange={(e) => setNum(Number(e.target.value))}
      />
      <ExpensiveComponent num={num} />
    </div>
  )
}

export default App

これをメモ化すると以下のようなコードになります。

メモ化のためのコードが追加されて、可読性が低下していることがわかります。

App.tsx
// メモ化したコード
import { useState, useMemo, useCallback } from 'react'

// 重い計算をする関数
const expensiveCalculation = (num: number) => {
  let result = 0
  for (let i = 0; i < 1000000000; i++) {
    result += num * i
  }
  return result
}

// 重たい計算を行うコンポーネント
const ExpensiveComponent = ({ num }: { num: number }) => {
  const result = useMemo(() => expensiveCalculation(num), [num])

  return <div>計算結果: {result}</div>
}

const App = () => {
  const [count, setCount] = useState(0)
  const [num, setNum] = useState(5)
  const handleClick = useCallback(() => {
    setCount(count + 1)
  }, [count])
  const doubledNum = useMemo(() => num * 2, [num])

  return (
    <div>
      <h1>Reactメモ化のサンプル</h1>
      <button onClick={handleClick}>カウント: {count}</button>
      <p>数値の2倍: {doubledNum}</p>
      <input
        type="number"
        value={num}
        onChange={(e) => setNum(Number(e.target.value))}
      />
      <ExpensiveComponent num={num} />
    </div>
  )
}

export default App

※ この例だとonChangeのときのvalue値変更後の処理は重いままなので、localStorageを併用したほうが良いです。

React 17と18のメモ化の自動化

React 17または18でもbabel-plugin-react-compilerとreact-compiler-runtimeをインストールすれば、メモ化の自動化が可能になります。

メモ化の自動化を試すためにReact 17をインストールしてみます。

React 18の場合は以下の「17」を「18」に変えて、main.tsxは変更しなくてOKです。

npm create vite@latest my-react-17 -- --template react-ts

cd my-react-17
npm un react react-dom
npm i react@17 react-dom@17
npm i -D babel-plugin-react-compiler react-compiler-runtime

React 17でcreateRootは使用不可なので、main.tsxを以下のコードに書き換えます。

main.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './index.css'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

次にvite.config.tsをbabel-plugin-react-compilerを読み込む設定に変更します。

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

const ReactCompilerConfig = {
  target: '17'
}

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['babel-plugin-react-compiler', ReactCompilerConfig]
        ],
      },
    }),
  ],
})

これらの設定が終わったら、App.tsxを「メモ化していないコード📝」に書き換えて確認してみてください。

「memo, useCallback, useMemo」を追加していませんが、メモ化が自動で行われているため、「カウント」のボタンを押しても動作が重くなっていないことが確認できます。

React 18でも同様の結果になります。

React 18 メモ化自動化