Next.js と TypeScript で、useMemo を学ぶ

TwitterFacebookHatena

TL;DR

こんにちは!今回は、React のメソッドの一つである useMemo を使った、アプリの更新方法について話しますね。

開発環境 バージョン
Next.js 13.4.4
TypeScript 5.0.4
Emotion 11.11.0
React 18.2.0

useMemo とは?

みなさん、お部屋をきれいにするために、掃除をすることはありますか?掃除をすると、物がきちんと整理されて、探し物が見つけやすくなったりしますよね。でも、毎回全ての部屋を掃除すると、時間がかかりますよね。

React の useMemo は、そのような掃除のようなものだと思ってください。useMemoは、特定の部屋(データ)だけをきれいに(計算する)して、他の部屋(データ)はそのままにしておく方法です。これにより、無駄な時間を使わずに、必要なところだけを素早く掃除(計算)することができます。

基本的な使い方は以下のようになります。

import React, { useMemo } from 'react'

// Props の型定義
type Props = {
  values: number[] // 数値の配列を受け取る
}

const CalculateSum = ({ values }: Props) => {
  // `values` の合計を計算する。計算結果はメモ化される。
  // 第2引数の依存配列に `values` を入れることで、`values` が変更された時のみ再計算を行う。
  const sum = useMemo(() => {
    return values.reduce((prev, curr) => prev + curr, 0)
  }, [values])

  // 計算結果を表示する
  return <div>合計: {sum}</div>
}

ここでは、useMemoを使って、valuesの合計を計算しています。useMemoの第一引数には、結果をメモ化(覚えておく)する関数を渡し、第二引数には依存配列を渡します。この依存配列の値が変わるときだけ、関数が再計算されます。つまり、valuesが変わらなければ、sumは再計算されずにメモ化された値が使われるのです。

useMemo の使い所

useMemoは、React のフックの一つで、計算結果を「記憶」(メモ化)しておくために使います。その名の通り、「メモ」を利用した最適化のための道具なのです。

計算とは何かと言うと、関数の結果を求めることや、オブジェクトや配列の新しいバージョンを作ることなどが含まれます。これらの操作は時にはコストが高くなることがあります。たとえば、大量のデータを処理する関数や、複雑なロジックを持つ関数がそうです。これらの関数が頻繁に呼び出されると、アプリケーションのパフォーマンスに影響を与えることがあります。

それでは、どういったシーンでuseMemoを使うのでしょうか。

高コストな計算を行うシーン

useMemoは、計算結果をメモ化するため、計算の結果が同じである限り、何度も同じ計算を行うことなく、前回の計算結果を再利用できます。これは、例えば大きなデータセットを扱うような場合や、複雑な計算を行う関数が頻繁に実行される場合に特に有用です。

参照の一貫性が重要なシーン

useMemoは生成された値の「参照」を保存します。これは、例えば子コンポーネントに props としてオブジェクトや配列を渡すような場合に役立ちます。子コンポーネントがこれらの props を依存配列に含むフック(useEffect, useMemo, useCallbackなど)を使用していると、親コンポーネントが再レンダリングされるたびに新しい参照が生成され、フックが不要に実行される可能性があります。useMemoを使用すると、依存配列内の値が変更されない限り、同じ参照を保持できるため、不要なフックの実行を防ぐことができます。

しかし、useMemoは計算の結果をメモ化するためのものであり、副作用(API 呼び出しやイベントハンドラなど)を含む処理には使用すべきではありません。副作用を含む処理にはuseEffectを使うべきです。

また、useMemoを使用するときは、メモ化自体にもコストがかかるため、計算のコストとメモ化のコストを比較して適切に使うことが重要です。計算のコストがそれほど高くない場合、あるいは計算が頻繁に行われない場合は、useMemoを使わない方がパフォーマンスが良い場合もあります。

useMemoは特に以下のようなシーンでよく使われます。

大規模な配列やオブジェクトの操作

例えば、大量のデータをフィルタリング、ソート、マッピングなどの操作をする場合、毎回レンダリングするたびにこれらの操作を実行するとパフォーマンスに影響を及ぼす可能性があります。そのような場合、useMemoを使って操作の結果をメモ化し、データが変更されない限り再計算を回避することができます。

const sortedList = useMemo(() => {
  return list.sort((a, b) => a - b)
}, [list])

複雑な計算の結果

フィボナッチ数列のような計算コストが高い演算や、複雑なアルゴリズムの結果をメモ化することで、パフォーマンスの改善が期待できます。

const fib = useMemo(() => {
  return calculateFibonacci(number)
}, [number])

参照等価性を保つ必要があるオブジェクトや関数

React の子コンポーネントに props としてオブジェクトや関数を渡す際、これらの値が再レンダリングのたびに新しく作られると、React はそれを新しい props として認識し、子コンポーネントも無駄に再レンダリングされます。これを回避するために、useMemouseCallbackを使って参照等価性を保つことがあります。

const handleClick = useCallback(
  (event) => {
    // ...
  },
  [dependency]
)

// or

const configObject = useMemo(() => {
  return { foo: 'bar', baz: 'qux' }
}, [dependency])

これらは一例であり、useMemoを使用するかどうかは、パフォーマンスに影響を及ぼす計算や操作があるかどうか、そしてそれがどの程度の頻度で発生するかによります。また、あまりにも頻繁にuseMemoを使用しすぎると、メモ化自体のオーバーヘッドがパフォーマンスに影響を及ぼすこともありますので注意が必要です。

例えば、リアルタイム検索を実装するとしましょう。

この場合、リアルタイム検索におけるuseMemoの使用は、結果の計算や操作がパフォーマンスに影響を及ぼす可能性がある場合に有効です。

たとえば、検索クエリに基づいて大量のデータをフィルタリングする必要がある場合、このフィルタリング操作は高コストである可能性があります。このフィルタリングを毎回レンダリングするたびに行うと、UI が遅くなる可能性があります。そのような場合、useMemoを使ってフィルタリング結果をメモ化し、クエリが変更されない限り再計算を避けることができます。

以下は、その一例です。

const SearchResults = ({ query, data }: Props) => {
  const results = useMemo(() => {
    // data が大量で filter の計算コストが高い場合に有効
    return data.filter((item) => item.includes(query))
  }, [query, data])

  // 結果のレンダリング
  return (
    <div>
      {results.map((result) => (
        <div key={result}>{result}</div>
      ))}
    </div>
  )
}

ただし、useMemoのオーバーヘッドが問題となる場合や、フィルタリングの計算コストがそれほど高くない場合は、useMemoなしでも問題ありません。また、サーバーサイドで検索結果をフィルタリングし、クライアントサイドにはすでにフィルタリングされた結果を送信するという設計もあります。この場合、useMemoを使用する必要はありません。

Next.js で、useMemo を実装

それでは、Next.js で useMemo を実装してみましょう。まず、以下のコードは /pages/index.tsx として作成します。

import { useMemo, useState } from 'react'
import CalculateSum from '../components/CalculateSum' // 合計を計算するコンポーネントをインポート

const Home = () => {
  // useState フックで values という名前の state を定義し、初期値を [1, 2, 3, 4, 5] とする
  const [values, setValues] = useState<number[]>([1, 2, 3, 4, 5])

  // 配列 values をシャッフルする関数を定義
  const handleShuffle = () => {
    // setValues を使って state を更新。シャッフルは Math.random() を使って行われる
    setValues(values.sort(() => Math.random() - 0.5))
  }

  return (
    <div>
      {/* シャッフルボタンを配置し、クリック時に handleShuffle 関数を実行 */}
      <button onClick={handleShuffle}>配列をシャッフル</button>
      {/* 合計値を計算するコンポーネントに現在の values を渡す */}
      <CalculateSum values={values} />
    </div>
  )
}

// Home コンポーネントをエクスポート
export default Home

ここでは、まず状態 values を定義し、handleShuffle 関数を使ってその配列をシャッフルします。そして、シャッフルされた配列 valuesCalculateSum コンポーネントに渡します。

次に、/components/CalculateSum.tsxとして以下のコードを作成します。

import { useMemo } from 'react' // React の useMemo フックをインポート

type Props = {
  values: number[] // props として number の配列を受け取る
}

const CalculateSum = ({ values }: Props) => {
  // useMemo フックを使って、values の合計をメモ化
  // これにより、values が変更されるまでの間、合計値の再計算がスキップされる
  const sum = useMemo(() => {
    return values.reduce((prev, curr) => prev + curr, 0) // 配列の各要素を足し合わせて合計値を算出
  }, [values])

  // 合計値を表示
  return <div>合計: {sum}</div>
}

export default CalculateSum // CalculateSum コンポーネントをエクスポート

CalculateSum コンポーネントでは、受け取った valuesuseMemo を使って合計し、その結果を表示します。useMemo により、values が変更されない限り、再計算を行わずに、メモ化した結果を再利用します。これにより、計算時間が節約できます。

useMemo を使った高度な例

useMemo は、高度な計算や重い処理を行う際に特に効果を発揮します。例えば、大量のデータをフィルタリングする際などに、結果をメモ化して再利用することで、パフォーマンスを向上させることができます。

以下に、大量のデータから特定の条件を満たすデータを抽出する際に useMemo を使用した例を示します。ファイル名は /components/FilterData.tsx とします。

import { useMemo } from 'react'

type Data = {
  id: number
  value: number
}

type Props = {
  data: Data[]
  threshold: number
}

const FilterData = ({ data, threshold }: Props) => {
  const filteredData = useMemo(() => {
    return data.filter((item) => item.value > threshold)
  }, [data, threshold])

  return (
    <div>
      {filteredData.map((item) => (
        <div key={item.id}>{item.value}</div>
      ))}
    </div>
  )
}

export default FilterData

このコンポーネントでは、data 配列から valuethreshold より大きい要素をフィルタリングします。フィルタリングの結果は filteredData としてメモ化されます。これにより、data または threshold が変更されない限り、フィルタリングの結果は再利用され、高負荷なフィルタリング処理を省くことができます。

Emotion での実装

最後に、スタイリングライブラリ Emotion を使用して useMemo を使った例を示します。以下のコードは、useMemo を使ってスタイルオブジェクトをメモ化し、再レンダリング時に新たなオブジェクトを生成することなく、既存のオブジェクトを再利用する例です。ファイル名は /components/StylizedButton.tsx とします。

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import { useMemo } from 'react'

type Props = {
  color: string
}

const StylizedButton = ({ color }: Props) => {
  const style = useMemo(
    () =>
      css`
        background-color: ${color};
        border: none;
        color: white;
        padding: 15px 32px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 16px;
        margin: 4px 2px;
        cursor: pointer;
      `,
    [color]
  )

  return <button css={style}>Click me!</button>
}

export default StylizedButton

StylizedButton コンポーネントでは、背景色を受け取り、その背景色に基づいてボタンのスタイルを定義します。スタイルオブジェクトは useMemo を使ってメモ化され、color が変更されない限り、再レンダリング時に新たなオブジェクトが生成されることはありません。これにより、再レンダリング時のパフォーマンスが向上します。

Next.js と TypeScript で、useMemo を学ぶ