React + TypeScript: memoで包んだコンポーネントでもコンテクストの更新により再レンダーされる

コンポーネントを[`memo`](https://qiita.com/FumioNonaka/items/c0427027dea12857c825)で包むと、親から渡されたプロパティ(`props`)が直前と変わらないかぎり再レンダーされません。ただし、コンテクストを使っていると(`useContext`)、`props`は前と同じでも、コンテクストが更新されれば再レンダーされます。簡単なコード例で、その動きと対応方法を探ってみましょう。


# ふたつの子コンポーネントでコンテクストを使う
つぎのモジュール`src/App.tsx`がつくるコンテクスト(`createContext`)は、ふたつの状態変数(`name`と`theme`)を`value`として子のコンポーネントツリーに与えています。子コンポーネントは、`NameEditor`と`Greeting`のふたつです。あとに示すように、どちらも`memo`でラップしてあります。なお、[メモ化した子コンポーネントにプロパティとして渡す関数(`handleChange`)は、`useCallback`に包みましょう](https://qiita.com/FumioNonaka/items/493ad3943d117c5ff1d6#usecallback%E3%81%A7%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%97%E3%81%9F%E9%96%A2%E6%95%B0%E3%82%92memo%E3%81%AB%E5%8C%85%E3%82%93%E3%81%A0%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%B8%E6%B8%A1%E3%81%99)。状態変数`theme`の値は[Switch theme]ボタン(`<button>`)で切り替わります。

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

const defaultContext = { name: 'Taylor', theme: 'dark' };
export const ThemeContext = createContext(defaultContext);
function App() {
	const [theme, setTheme] = useState(defaultContext.theme);
	const [name, setName] = useState(defaultContext.name);
	const handleClick = useCallback(() => {
		setTheme(theme === 'dark' ? 'light' : 'dark');
	}, [theme]);
	const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
		({ target: { value } }) => {
			setName(value);
		},
		[name]
	);
	return (
		<ThemeContext.Provider value={{ name, theme }}>
			<button onClick={handleClick}>Switch theme</button>{' '}
			<NameEditor onChange={handleChange} />
			<Greeting />
		</ThemeContext.Provider>
	);
}

export default App;
```

モジュール`src/Greeting.tsx`のコンポーネント`Greeting`が、コンテクストから取り出す(`useContext`)のはふたつの値`name`と`theme`です。`name`は表示するテキスト、`theme`はカラー設定のクラス(`className`)を変更します。

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

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

モジュール`src/NameEditor.tsx`のコンポーネント`NameEditor`が、コンテクストから得る(`useContext`)のが`name`です。`props`として受け取った`onChange`で、値を入力フィールド(`<input type="text">`)のテキストに更新します。

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

type Props = {
	onChange: ChangeEventHandler;
};
export const NameEditor: FC<Props> = memo(({ onChange }) => {
	console.log('NameEditor was rendered at', new Date().toLocaleTimeString());
	const { name } = useContext(ThemeContext);
	return <input type="text" value={name} onChange={onChange} />;
});
```

つぎのサンプル001でお試しいただくと、とくに動きに問題はないでしょう。ふたつの子コンポーネントにはレンダリングを確かめる`console.log`が加えてあるので、コンソール出力をご覧ください。入力フィールドのテキストを編集すると、両コンポーネントが再レンダーされます。どちらも、更新された`name`の値を用いていますので、想定された結果です。

ところが、親コンポーネントの[Switch theme]ボタンをクリックすると、要素のカラーが変わる`Greeting`はよいとして、コンテクストの`theme`など使っていない`NameEditor`までレンダリングされます。これは、コンポーネントの用いるコンテクスト`ThemeContext`が更新されたからです。

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

# 子コンポーネントを包んだ親から`useContext`を呼び出す
前掲サンプル001でしたら、子コンポーネントの描画がとくに重いとは感じません。けれど、コンテクストの抱える値が増え、子コンポーネントのレンダリング負荷も高まると、特定の値の更新時にのみ再レンダーしたいと考えるかもしれません。

そのような場合には、子コンポーネントは新たな親(`Container`)で包みましょう。

```react:src/App.tsx
/* import { Greeting } from './Greeting';
import { NameEditor } from './NameEditor'; */
import { Container } from './Container';

function App() {

	return (
		<ThemeContext.Provider value={{ name, theme }}>

			{/* <NameEditor onChange={handleChange} />
			<Greeting /> */}
			<Container onChange={handleChange} />
		</ThemeContext.Provider>
	);
}
```

そして、`useContext`はラップした親から呼び出してください。コンテクストから取り出した子コンポーネントが必要な値は、それぞれ`props`として与えるのです。

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

type Props = {
	onChange: ChangeEventHandler<HTMLInputElement>;
};
export const Container: FC<Props> = memo(({ onChange }) => {
	const { name, theme } = useContext(ThemeContext);
	return (
		<>
			<NameEditor name={name} onChange={onChange} />
			<Greeting name={name} theme={theme} />
		</>
	);
});
```

子コンポーネントは、もはやコンテクストを使いません。`props`であれば、コンポーネントのメモ化が働きます。受け取った値が変わらないかぎり、再レンダーされません。

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

// import { ThemeContext } from './App';

type Props = {
	name: string;
	theme: string;
};
// export const Greeting: FC<Props> = memo(() => {
export const Greeting: FC<Props> = memo(({ name, theme }) => {

	// const { name, theme } = useContext(ThemeContext);
	return <h3 className={theme}>Hello, {name}!</h3>;
});
```

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

type Props = {
  name: string;
  onChange: ChangeEventHandler;
};
// export const NameEditor: FC<Props> = memo(({ onChange }) => {
export const NameEditor: FC<Props> = memo(({ name, onChange }) => {
  console.log('NameEditor was rendered at', new Date().toLocaleTimeString());
  // const { name } = useContext(ThemeContext);
  return <input type="text" value={name} onChange={onChange} />;
});
```

前掲コードの書き替えを加えたつぎのサンプル002をお試しください。動きはサンプル001と変わりません。けれど、[Switch theme]ボタンをクリックしても、`NameEditor`コンポーネントは再レンダーされなくなりました。

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