はじめに
Reactアプリケーションの開発において、パフォーマンスの最適化は重要な課題の一つです。
特に、大規模なアプリケーションやデータの更新が頻繁に発生するアプリケーションでは、レンダリングのパフォーマンスが問題になることがあります。
そこで使えるのがuseMemoとuseCallbackを使ったメモ化です。
この記事では、メモ化の基本的な概念から、具体的な使用方法、よくある間違いまで、幅広く解説します。
メモ化とは
メモ化(Memoization)とは、関数の結果を一時的に保存し、同じ入力に対しては保存された結果を返すことで、関数の実行を高速化するテクニックです。
つまり、一度計算した結果を覚えておいて、同じ計算を繰り返さないようにするのです。
Reactにおけるメモ化は、主にコンポーネントのレンダリングに関連します。
Reactは、状態やプロップスが変更されると、関連するコンポーネントを自動的に再レンダリングします。
しかし、大規模なアプリケーションでは、不必要な再レンダリングが発生し、パフォーマンスの低下を引き起こすことがあります。
そこで、メモ化を使って、コンポーネントの再レンダリングを最適化します。
useMemoの使い方
useMemoは、メモ化された値を返すフックです。
useMemoの基本的な使い方は以下の通りです。
const memoizedValue = useMemo(() => calc(a, b), [a, b]);
第1引数にはメモ化する値を計算する関数を渡します。
第2引数にはメモ化する値が依存する変数を指定します。
これは依存配列と呼ばれ、依存配列の要素が変更された場合にのみ、メモ化された値が再計算されます。
具体的な例を見てみましょう。
import { useMemo, useState } from "react";
const complexCalc = (num: number): number => {
console.log("計算中...");
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
const MyComponent = () => {
const [count, setCount] = useState<number>(0);
const [todos, setTodos] = useState<string[]>([]);
const calculation = useMemo(() => complexCalc(count), [count]);
const increment = (): void => {
setCount((c) => c + 1);
};
const addTodo = (): void => {
setTodos((t) => [...t, "新しいTodo"]);
};
return (
<div>
<div>
<h2>My Todos</h2>
{todos.map((todo, index) => {
return <p key={index}>{todo}</p>;
})}
<button onClick={addTodo}>Todo追加</button>
</div>
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
<h2>Complex Calculation</h2>
{calculation}
</div>
</div>
);
};
export default MyComponent;
この例では、complexCalcという複雑な計算を行う関数があります。
(単純化のため、単にたくさん足し算するだけの関数にしています。)
この関数の結果をuseMemoでメモ化し、calculationに格納しています。
そしてuseMemoの第2引数である依存配列にはcountを指定しています。
これは、countの値が変更された場合にのみ、complexCalcが再実行されることを意味します。
このコンポーネントでは、countの値を増加させるボタンと、TODOリストにアイテムを追加するボタンがあります。
いずれもuseStateを使用しているので、値が更新されるとコンポーネントは再描画されます。
しかし、countが更新されるとcalculationの値が再計算されますが、TODOリストが更新されても、calculationは再計算されません。
このように、useMemoを使うことで、高コストな計算の結果をメモ化し、不必要な再計算を避けることができます。
useCallbackの使い方
useCallbackは、メモ化されたコールバック関数を返すフックです。
useMemoと似ていますが、値の代わりに関数をメモ化します。
useCallbackの基本的な使い方は以下の通りです。
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
第1引数には、メモ化するコールバック関数を渡します。
第2引数の依存配列には、コールバック関数が依存する変数を指定します。
依存配列の要素が変更された場合にのみ、新しいコールバック関数が生成されます。
具体的な例を見てみましょう。
import React, { useState, useCallback, memo } from "react";
interface Todo {
id: number;
text: string;
}
interface TodosProps {
todos: Todo[];
addTodo: () => void;
}
const MyComponent = () => {
const [count, setCount] = useState<number>(0);
const [todos, setTodos] = useState<Todo[]>([]);
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
const addTodo = useCallback(() => {
setTodos((t) => [...t, { id: Date.now(), text: "新しいTodo" }]);
}, []);
return (
<>
<Todos todos={todos} addTodo={addTodo} />
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
</div>
</>
);
};
const Todos: React.FC<TodosProps> = memo(({ todos, addTodo }) => {
return (
<>
<h2>My Todos</h2>
{todos.map((todo) => {
return <p key={todo.id}>{todo.text}</p>;
})}
<button onClick={addTodo}>Todo追加</button>
</>
);
});
export default MyComponent;
この例では、Todosコンポーネントをmemoでラップし、プロップスが変更された場合にのみ再レンダリングされるようにしています。
親コンポーネントのMyComponentでは、incrementとaddTodoの2つのコールバック関数をuseCallbackでメモ化しています。
incrementはsetCountに依存し、addTodoはtodosに依存しています。
これにより、countが変更されても、addTodo関数は再生成されず、Todosコンポーネントは再レンダリングされません。
このように、useCallbackを使うことで、コールバック関数をメモ化し、不必要な再レンダリングを避けることができます。
メモ化の注意点
メモ化は強力なテクニックですが、正しく使わないと逆効果になることがあります。
ここでは、メモ化を使う際の注意点をいくつか紹介します。
1. 依存配列の設定
useMemoやuseCallbackの第2引数である依存配列には、メモ化する値や関数が依存するすべての変数を指定する必要があります。
依存配列が不完全だと、メモ化が正しく機能しません。
2. メモ化のコスト
メモ化自体にもコストがかかります。
メモ化する値や関数の計算コストが低い場合、メモ化のオーバーヘッドのほうが大きくなることがあります。
メモ化を使う前に、本当にメモ化が必要かを検討しましょう。
3. メモ化と等価性
useMemoやuseCallbackは、依存配列の要素の等価性をもとにメモ化された値や関数を再利用します。
このため、依存配列に参照型の値(オブジェクトや配列)を含める場合は注意が必要です。
参照型の値は、内容が同じでもメモリ上の参照が異なるため、等価性の判定に失敗します。
その結果、メモ化が機能しなくなります。
この問題を避けるには、参照型の値をプリミティブな値に変換するか、依存配列から除外する必要があります。
まとめ
useMemoとuseCallbackメモ化の手法について、基礎的な使い方を紹介しました。
まだまだ実務でうまく使うことができていないReactフックなのですが、いざというときにしっかり使えるよう理解しておきたいと思いました。