React + TypeScript: memoでコンポーネントの再レンダーをpropsが変わらないかぎり省く

`memo`で包んだコンポーネントは、受け取る`props`が変わらないかぎり再レンダーされません。

本稿はReact公式サイト「[memo](https://react.dev/reference/react/memo)」にもとづき、`memo`はどう使うのか、およびどのような場合に使うとよいのかを解説します。説明内容と順序は、公式ドキュメントにしたがいました。ただし、解説はわかりやすく改め、またコード例とサンプル([StackBlitz](https://stackblitz.com/))はTypeScriptを加えたうえで修正した部分が少なくありません。


# 構文

```react
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
```

コンポーネントを`memo`でラップすると、[メモ化](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%A2%E5%8C%96)されたコンポーネントが返されます。メモ化されたコンポーネントは、親コンポーネントのレンダリングにかかわらず、受け取る`props`が変わらないかぎり通常再レンダーされません。ただし、Reactがそれでもレンダリングすることはあり得ます。メモ化はパフォーマンス最適化のひとつであって、保証まではしません。

```react
import { memo } from 'react';

const SomeComponent = memo((props) => {

});
```

`memo`の構文はつぎのとおりです。

```react
memo(Component, arePropsEqual?) 
```

### 引数

* `Component`: メモ化するコンポーネント。
    - コンポーネントそのものは変更されません。返されるのはメモ化された新たなコンポーネントです。
    - コンポーネントは関数や[`forwardRef`](https://ja.react.dev/reference/react/forwardRef)によるものも含まれ、任意の有効なReactコンポーネントが渡せます。
* `arePropsEqual`(省略可能): 直前と今回のふたつの`props`を引数にとる関数。戻り値は`props`が変わったかどうかを示す`boolean`値です。
    - `true`の場合: `props`は変わっていないと評価し、キャッシュされたコンポーネントのまま、再レンダーしません。つまり、コンポーネントの出力も動きも直前と同じです。
    - `false`の場合: 直前と`props`が変わったと評価され、コンポーネントは再レンダーされまます。
    - 通常は、この関数を与える必要はありません。デフォルトで、ふたつの`props`の値それぞれが[`Object.is`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/is)により比べられます。

### 戻り値
第1引数のコンポーネントをメモ化した新たなReactコンポーネントです。

* 動作は第1引数に渡したコンポーネントと変わりません。
* ただし、親が再レンダーされたとき、渡された`props`が直前と同じかどうかを確かめます。変わらなければ、Reactはコンポーネントを再レンダーしません。


# 使い方
## 受け取った`props`が直前と変わっていなければコンポーネントを再レンダーしない
Reactはデフォルトでは、親コンポーネントのレンダリングが起こると、つねに子を再起的に再レンダーします。`memo`が返すのは、無駄な再レンダーを省くメモ化された新たなコンポーネントです。Reactは、コンポーネントの受け取った`props`が、直前と変わっていないかを確かめます。同じであれば、再レンダーしません。

コンポーネントを`memo`で包むと、返されるのがメモ化されたコンポーネントです。戻り値は、もとのコンポーネント(第1引数)と同じように使えます。

```react
export const Greeting: FC<Props> = memo(({ name }) => {
	return <h1>Hello, {name}!</h1>;
});
```

Reactコンポーネントの[レンダーロジックは、つねに純粋に保たなければなりません](https://qiita.com/FumioNonaka/items/2ba530a906bf97480f80#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E7%B4%94%E7%B2%8B%E3%81%AB%E4%BF%9D%E3%81%A4keeping-components-pure)。`props`や状態およびコンテクストが変わらないかぎり、つねに同じ出力を返すということです。`memo`を用いることにより、Reactにコンポーネントが純粋であると伝わります。`props`が直前と変わらなければ、Reactはコンポーネントを再レンダーしません。ただし、コンポーネントの状態や、用いたコンテクストが更新されたときは、再レンダリングが起こります。

つぎのコード例で、モジュール`src/Greeting.tsx`のコンポーネント(`Greeting`)は`memo`で包みました。したがって、渡されるプロパティ`name`が変わらなければ再レンダーされません。レンダリングされているか確認するため、`console.log`を加えました。

```react:src/Greeting.tsx
import { memo } from 'react';
import type { FC } from 'react';

type Props = {
	name: string;
};
export const Greeting: FC<Props> = memo(({ name }) => {
	console.log('Greeting was rendered at', new Date().toLocaleTimeString());
	return (
		<h3>
			Hello{name && ', '}
			{name}!
		</h3>
	);
});
```

親コンポーネント(`App`)のモジュール`src/App.tsx`のコードはつぎのとおりです。状態変数は`name`と`address`のふたつで、テキストフィールド(`<input>`要素)への入力により値が変わります。子コンポーネント`Greeting`に渡したプロパティは、ふたつのうちの`name`です。

```react:src/App.tsx
import { useState } from 'react';
import { Greeting } from './Greeting';

function App() {
	const [name, setName] = useState('');
	const [address, setAddress] = useState('');
	return (
		<>
			<label>
				Name{': '}
				<input
					value={name}
					onChange={({ target: { value } }) => setName(value)}
				/>
			</label>
			<label>
				Address{': '}
				<input
					value={address}
					onChange={({ target: { value } }) => setAddress(value)}
				/>
			</label>
			<Greeting name={name} />
		</>
	);
}

export default App;
```

つぎのサンプル001で試して、コンソール出力を確かめてみてください。`name`のテキストフィールドに入力すると、子コンポーネント`Greeting`は再レンダーされるでしょう。けれど、`address`の値は、`Greeting`のプロパティに含まれません。`address`のテキストフィールドに入力しても、子コンポーネントの再レンダーは省かれるのです。

#### サンプル001■React + TypeScript: memo 01
https://stackblitz.com/edit/react-typescript-memo-01?file=src%2FGreeting.tsx

[注記] **`memo`はパフォーマンスを最適化するためにお使いください**。`memo`なしにはコードが動かないという場合は、まず原因を探って修正するべきです。そのうえで、必要があれば`memo`を用いましょう。

### `memo`はどこに使うべきか
メモ化はどこに用いるべきで、どういうときは不要か、大まかに比べるなら大きくふたつです。

* インタラクションがさほど多くなければ、`memo`を使わななくて構いません。
    - ページの切り替えや画面の一部の変更で済む場合。
* 細かなインタラクションが頻繁に生じるときは、メモ化は有効でしょう。
    - 描画エディターのアプリケーションで、図形を移動させるなど。

`memo`による最適化が役立つために必要な条件は、つぎのふたつです。

* コンポーネントがまったく同じ`props`で頻繁に再レンダーされる。
* 再レンダーするロジックの負荷が高い。

コンポーネントが再レンダーされても遅れはとくに感じられない場合、`memo`は要りません。さらに、レンダー中に定めたオブジェクトや関数をそのまま渡していると、コンポーネントが受け取った`props`は毎回異なると評価され、`memo`は無意味です。[`useMemo`](https://qiita.com/FumioNonaka/items/86f6a91b98844beaff0d)や[`useCallback`](https://qiita.com/FumioNonaka/items/493ad3943d117c5ff1d6)と併せて使うことをお考えください。

こうした場合以外は、コンポーネントを`memo`でラップしても、効果はないでしょう。かといって、問題となることも考えにくいです。とりあえず、メモ化してしまうという開発チームもあります。デメリットとしては、コードが読みにくくなることです。また、レンダーごとに変わる値がひとつでもコンポーネントに含まれていれば、メモ化する意味はありません。

#### 不要なメモ化を避けるための5つの原則
メモ化はつねに有効とはかぎりません。つぎの5つの原則にしたがえば、不要なメモ化が避けられるでしょう。

1. コンポーネントが子コンポーネントを視覚的にラップするときは、[子のJSXは`children`として受け取る](https://ja.react.dev/learn/passing-props-to-a-component#passing-jsx-as-children)ようにします。子を包む親コンポーネントが自身の状態を更新しても、Reactは子の再レンダーは要らないとわかるからです。
2. 状態はできるだけローカルに持ち、無闇にコンポーネントツリー上を引き上げないようにします(「[React + TypeScript: コンポーネント間で状態を共有する](https://qiita.com/FumioNonaka/items/611bd5e0cd254c0e9ea0)」参照)。フォーム入力や項目へのホバーといった状態は、頻繁に変わるインタラクションです。ツリーのトップやグローバルの状態ライブラリに保持しないでください。
3. [レンダーロジックを純粋に保ちましょう](https://qiita.com/FumioNonaka/items/2ba530a906bf97480f80#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E7%B4%94%E7%B2%8B%E3%81%AB%E4%BF%9D%E3%81%A4keeping-components-pure)。コンポーネントの再レンダーで意図しない動きになったり、表示に明らかな問題が起こるとすれば、それはバグです。メモ化で避けようとするのでなく、原因をつきとめて修正してください。
4. [不必要に状態を更新するエフェクトは避けましょう](https://qiita.com/FumioNonaka/items/f6bfe4ca77e590f9e73f)。Reactアプリケーションでパフォーマンスの問題を引き起こす原因の多くが、エフェクトによる連鎖的な状態の更新です。コンポーネントはそのたびに再レンダーされてしまいます。
5. [エフェクトから要らない依存値は除いてください](https://qiita.com/FumioNonaka/items/1ca250ec6bf7234a7afd)。メモ化しなくても、オブジェクトや関数をエフェクトの中あるいは外に移すだけで、簡単に解決できることもあります。

以上の原則にしたがっても、遅いと感じるインタラクションが残るかもしれません。そのような場合には、[React Developer Tools](https://ja.react.dev/learn/react-developer-tools)のProfilerパネルをお使いください。どのコンポーネントをメモ化すればもっとも効果的かが確かめられます。このように開発を進めることで、デバッグしやすく、わかりやすいコードが書けるでしょう。[React公式サイトが推奨する](https://ja.react.dev/reference/react/memo#should-you-add-memo-everywhere:~:text=%E3%81%93%E3%82%8C%E3%82%89%E3%81%AE%E5%8E%9F%E5%89%87%E3%82%92%E5%AE%88%E3%82%8B%E3%81%93%E3%81%A8%E3%81%A7%E3%80%81%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E3%82%84%E7%90%86%E8%A7%A3%E3%81%8C%E5%AE%B9%E6%98%93%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%9F%E3%82%81%E3%80%81%E5%B8%B8%E3%81%AB%E5%8E%9F%E5%89%87%E3%81%AB%E5%BE%93%E3%81%86%E3%81%93%E3%81%A8%E3%82%92%E3%81%8A%E3%81%99%E3%81%99%E3%82%81%E3%81%97%E3%81%BE%E3%81%99%E3%80%82)理由です。将来に向けては、自動的にメモ化する研究も進められています(「[React without memo](https://www.youtube.com/watch?v=lGEMwh32soc)」参照)。

### 状態が変わるとメモ化されたコンポーネントは更新される
メモ化されたコンポーネントが確かめるのは、親から渡された`props`の直前との違いです。`props`とは別に、自身の状態が変われば再レンダーされます。

前掲サンプル001のモジュール`src/Greeting.tsx`のコンポーネント(`Greeting`)に`useState`で状態変数(`greeting`)を加えました。さらに、その値は新たに定める子コンポーネント(`GreetingSelector`)にプロパティ(`value`)として渡しています。状態変数値を変える(`setGreeting`)のは、子コンポーネントです。

```react:src/Greeting.tsx
// import { memo } from 'react';
import { memo, useState } from 'react';

import { GreetingSelector } from './GreetingSelector';

export const Greeting: FC<Props> = memo(({ name }) => {

	const [greeting, setGreeting] = useState('Hello');
	return (
		<>
			<h3>
				{/* Hello{name && ', '} */}
				{greeting}
				{name && ', '}
				{name}!
			</h3>
			<GreetingSelector value={greeting} onChange={setGreeting} />
		</>
	);
});
```

モジュール`src/GreetingSelector.tsx`のコンポーネント(`GreetingSelector`)は、親から状態変数値`value`とその設定関数`onChange`をプロパティとして受け取ります。設定を変えるのはふたつのラジオボタン(`<input type="radio"`)です。

```react:src/GreetingSelector.tsx
import type { FC } from 'react';

type Props = {
	onChange: (greeting: string) => void;
	value: string;
};
type InputItemmProps = Props & {
	greeting: string;
	text: string;
};
const InputItem: FC<InputItemmProps> = ({
	onChange,
	greeting,
	text,
	value,
}) => {
	return (
		<label>
			<input
				type="radio"
				checked={value === greeting}
				onChange={() => onChange(greeting)}
			/>
			{text}
		</label>
	);
};
export const GreetingSelector: FC<Props> = ({ value, onChange }) => {
	return (
		<>
			<InputItem
				onChange={onChange}
				greeting="Hello"
				text="Regular greeting"
				value={value}
			/>
			<InputItem
				onChange={onChange}
				greeting="Hello and welcome"
				text="Enthusiastic greeting"
				value={value}
			/>
		</>
	);
};
```

コンポーネント`GreetingSelector`のラジオボタンを切り替えると、親の`Greeting`の状態(`greeting`)が変わります。`greeting`は`memo`で包まれており、受け取る`props`は変わらなくとも、状態が更新されればコンポーネントは再レンダーされるのです(サンプル002)。

#### サンプル002■React + TypeScript: memo 02
https://stackblitz.com/edit/react-typescript-memo-02?file=src%2FGreeting.tsx

ただし、状態設定関数に現行と同じ値を渡して呼び出した場合は、コンポーネントのメモ化にかかわらず、再レンダーされません。関数コンポーネントは呼び出されても、レンダリングが省かれるのです。


### コンテクストが変わるとメモ化されたコンポーネントは更新される
メモ化されたコンポーネントが確かめるのは、親から渡された`props`の直前との違いです。`props`とは別に、使っているコンテクストが変われば再レンダーされます。

つぎのモジュール`src/App.tsx`は、`createContext`でコンテクスト(`ThemeContext`)をつくっています。プロバイダに`value`として与えるのは状態変数値`theme`です(ボタンクリックで値は切り替わります)。子コンポーネント`Greeting`に渡すプロパティの`name`は決め打ち(`"Taylor"`)なので、値が変わることはありません。

```react:src/App.tsx
import { createContext, useCallback, useState } from 'react';
import { Greeting } from './Greeting';

const defaultTheme = 'dark';
export const ThemeContext = createContext(defaultTheme);
function App() {
	const [theme, setTheme] = useState(defaultTheme);
	const handleClick = useCallback(() => {
		setTheme(theme === 'dark' ? 'light' : 'dark');
	}, [theme]);
	return (
		<ThemeContext.Provider value={theme}>
			<button onClick={handleClick}>Switch theme</button>
			<Greeting name="Taylor" />
		</ThemeContext.Provider>
	);
}

export default App;
```

モジュール`src/Greeting.tsx`の子コンポーネント(`Greeting`)は、`useContext`で取り出したコンテクストの値(`theme`)に応じて、要素のクラス(`className`)を切り替えています(要素のカラー変更)。コンポーネントは`memo`で包みました。

```react:src/Greeting.tsx
import { memo, useContext } from 'react';
import type { FC } from 'react';
import { ThemeContext } from './App';

type Props = {
	name: string;
};
export const Greeting: FC<Props> = memo(({ name }) => {
	console.log('Greeting was rendered at', new Date().toLocaleTimeString());
	const theme = useContext(ThemeContext);
	return <h3 className={theme}>Hello, {name}!</h3>;
});
```

つぎのサンプル003で試すと、親コンポーネントの[Switch theme]ボタンをクリックするたびに、子の`Greeting`は再レンダーされます(コンソール出力をお確かめください)。前述のとおり、`Greeting`はメモ化し、渡されるプロパティ(`name`)も変わりません。けれど、コンテクストの値(`theme`)が切り替わっています。すると、それを用いるコンポーネントは再レンダーされるのです。

#### サンプル003■React + TypeScript: memo 03
https://stackblitz.com/edit/react-typescript-memo-03?file=src%2FApp.tsx

開発が進むにつれ、コンテクストのもつ値は膨らんでくるかもしれません。すると、子コンポーネントの再レンダーは、コンテクストの特定の値が変わったときに留めたい場合もあるでしょう。そういうときは、子コンポーネントを、新たにつくる親でラップしてください。`useContext`は親コンポーネントから呼び出すのです。そのうえで、子コンポーネントに必要な値は、それぞれプロパティとして与えます。こうすれば、コンポーネントのメモ化も働くでしょう。具体的なコードとサンプルについては、「[子コンポーネントを包んだ親からuseContextを呼び出す](https://qiita.com/FumioNonaka/items/ba6bb8047f15d2024107#%E5%AD%90%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E5%8C%85%E3%82%93%E3%81%A0%E8%A6%AA%E3%81%8B%E3%82%89usecontext%E3%82%92%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%99)」をお読みください。

### `props`はできるだけ変えない
`memo`で包んだコンポーネントは、受け取った`props`のいずれかが浅い(*shallow*)比較で直前と異なるとき再レンダーされます。Reactがコンポーネントの`props`すべてを前回の値と比べるとき用いるのは[`Object.is`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/is)です。とくに、オブジェクトは、プリミティブ値と異なり、参照が比較されることにご注意ください。

* `Object.is(3, 3)`: `true`
* `Object.is({}, {})`: `false`

#### `useMemo`で`props`の値を無駄につくり直さない
`memo`の有効性を高めるには、子が受け取る`props`の値はできるだけ変えないようにします。親コンポーネントが値を[`useMemo`](https://qiita.com/FumioNonaka/items/86f6a91b98844beaff0d)に包んで渡せば、オブジェクトであっても無駄につくり直されません。

```react
const Page: FC = () => {
	const [name, setName] = useState('Taylor');
	const [age, setAge] = useState(42);
	const person = useMemo(
		() => ({ name, age }),
		[name, age]
	);
	return <Profile person={person} />;
}
const Profile: FC<Props> = memo(({ person }) => {

});
```

#### 必要な値だけを`props`として渡す
可能であれば、必要な値だけを子に`props`として与えることです。使う値がオブジェクトの中の一部なら、丸ごと`props`で渡すことはありません。

```react
const Page: FC = () => {
	const [name, setName] = useState('Taylor');
	const [age, setAge] = useState(42);
	return <Profile name={name} age={age} />;
}
const Profile: FC<Props> = memo(({ name, age }) => {

});
```

#### `props`を更新の少ない値に変換して渡す
親から`props`で受け取ったオブジェクトそのものを使うのでなく、処理や評価した結果がほしいという場合もあるでしょう。親コンポーネントの側でできるのなら、結果だけ渡せば`props`の更新は減らせるかもしれません。

つぎのコード例では、子コンポーネントが受け取るのは、頻繁に変わるかもしれない値ではなく、値があるかどうかを示す`boolean`値(`hasGroups`)です。

```react
const GroupsLanding: FC<Props> ({ person }) => {
	const hasGroups = person.groups !== null;
	return <CallToAction hasGroups={hasGroups} />;
}

const CallToAction: FC<CallToActionProps> = memo(({ hasGroups }) => {

});
```

#### `props`で渡す関数の更新を省く
コンポーネントの中で定義された関数は、デフォルトではオブジェクトと同じように、レンダリングのたびにつくり直されます。メモ化された子コンポーネントに`props`で関数を渡す場合には、無駄な更新は避けなければなりません。考えられるのは、つぎのふたつの対応です。

* 関数をコンポーネントの外で定めてください。
    - コンポーネント外の関数は、再レンダリングでつくり直されません。
* コンポーネント内の関数定義を[`useCallback`](https://qiita.com/FumioNonaka/items/493ad3943d117c5ff1d6)で包みましょう。
    - 依存配列が変わらないかぎり、関数は再定義されません。


### `memo`の第2引数にカスタム比較関数を定める
子コンポーネントが渡された`props`を直前と比べるとき、デフォルトの浅い比較(``Object.is``)では足りないことがありえます。その場合、`memo`の第2引数として渡せるのがカスタム比較関数です。比較関数の受け取るふたつの引数がそれぞれ前回と今回の`props`、ふたつに変わりがないかを`boolean`値で返します。値が同じと評価するなら戻り値は`true`で、再レンダーされません。

```react
const Chart: FC<Props> = memo(({ dataPoints }) => {
	// ...
}, arePropsEqual);

const arePropsEqual = (oldProps: Props, newProps: Props) => {
	return (
		oldProps.dataPoints.length === newProps.dataPoints.length &&
		oldProps.dataPoints.every((oldPoint, index) => {
			const newPoint = newProps.dataPoints[index];
			return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
		})
	);
}
```

`memo`に第2引数で比較関数を与えた場合、効果はブラウザのデベロッパーツールで[パフォーマンス]パネルからお確かめください。なお、React は本番モードで動作させましょう。

#### カスタム比較関数を使う場合の注意
`memo`の第2引数(`arePropsEqual`)にカスタム比較関数を与えた場合、`props`の値はオブジェクトだけでなく、**関数も含めてすべて前回と正しく比べなければなりません**。

関数で注意すべきことは、親コンポーネントの`props`と状態を[クロージャに閉じ込める](https://developer.mozilla.org/docs/Web/JavaScript/Closures)ことです。クロージャ内が異なるのに比較結果の戻り値を`true`にすると、きわめて見つけにくいバグの原因となります。たとえば、実際には`oldProps.onClick !== newProps.onClick`なのに`true`を返すと、コンポーネントは`onClick`ハンドラ内で以前のレンダー時の`props`と状態を「見続ける」ことになるからです。

`props`のデータ構造の深さが100%わかっている場合以外、カスタム比較関数(`arePropsEqual`)で等価性を調べるのはお勧めしません。**等価性を深く確かめようとすることは過剰な負荷につながります**。あとでデータ構造が変わったら、数秒間フリーズしてしまうかもしれません。


# トラブルへの対応
## `props`に渡しているのは同じ内容のオブジェクト・配列・関数なのにメモ化したコンポーネントが再レンダーされる
Reactが前回と今回の`props`が等しいかを調べるのは浅い比較です。つまり、親コンポーネントが再レンダーのたびにオブジェクトや配列をつくり直せば、中身のプロパティや要素が同じであっても、Reactは参照にもとづいて異なると評価します。また、コンポーネント内に定められた関数も、デフォルトではレンダー時に再定義されるので同じとはみなされません。これを避けるためには、前述「[`props`はできるだけ変えない](https://qiita.com/FumioNonaka/items/c0427027dea12857c825#props%E3%81%AF%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%A0%E3%81%91%E5%A4%89%E3%81%88%E3%81%AA%E3%81%84)」をご参照ください。