React + TypeScript: useContextフックでコンポーネントツリーに渡された情報を受け取る

[コンテクスト](https://ja.react.dev/learn/passing-data-deeply-with-context)(context)は、コンポーネントツリーに情報を渡す仕組みです。直接の子でなく、深い階層にある子コンポーネントであっても、`useContext`フックによりコンテクストの値を読み取り、その更新が受け取れます(subscribe)。

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


# 構文

```react
const value = useContext(SomeContext)
```

フック`useContext`は、コンポーネントのトップレベルで呼び出してください。フックが読み込んだコンテクストから、更新された値を受け取ります。

```react
import { useContext } from 'react';
import { ThemeContext } from './App';

export const MyComponent: FC = () => {
	const theme = useContext(ThemeContext);

}
```

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

```react
useContext(SomeContext) 
```

## 引数

* `SomeContext`: あらかじめ[`createContext`](https://ja.react.dev/reference/react/createContext)でつくったコンテクスト。
    - 異なるモジュールの子コンポーネントから用いるには、コンテクストを`export`してください。
    - コンテクストそのものは情報をもちません。コンポーネントから提供(provide)し、読み取れる「情報の種別」です。

## 戻り値
`useContext`フックを呼び出したコンポーネントに応じたコンテクストの値が返されます。

* コンポーネントツリーを上層に向けて探し、もっとも近い`SomeContext.Provider`に渡された`value`の値です。
* ツリーの上層にプロバイダが見つからない場合は、コンテクストをつくった[`createContext`](https://ja.react.dev/reference/react/createContext)の引数に渡したデフォルト値(`defaultValue`)となります。

戻り値は、つねにコンテクストの最新値です。Reactは、コンテクストが更新されると、読み取っているコンポーネントを自動的に再レンダーします。

## 注意

* `useContext`の呼び出しは、そのコンポーネント自身の戻り値(JSX)に加えられたプロバイダの値を対象としません。`useContext`を呼び出したコンポーネントから、**ツリーの上層に向かって**`<Context.Provider>`が探されるからです。
* コンテクストプロバイダの受け取る`value`が変わると、コンポーネントツリー内でそのコンテクストを用いるすべての子コンポーネントはReactにより自動的に再レンダーされます。
    - `value`が変わったかどうかの比較は[`Object.is`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/is)です。
    - 子コンポーネントを[`memo`](https://qiita.com/FumioNonaka/items/c0427027dea12857c825)で包んでも、コンテクストは引数に受け取る`props`ではないので、再レンダーは省かれません(「[React + TypeScript: memoで包んだコンポーネントでもコンテクストの更新により再レンダーされる](https://qiita.com/FumioNonaka/items/ba6bb8047f15d2024107)」参照)。
* ビルドシステムの生成する出力の中でモジュールが重複する場合(シンボリックリンクで起こり得ます)、コンテクストは壊れるかもしれないことにご注意ください。
    - コンテクストによる値の受け渡しが動作するには、提供するために用いる`SomeContext`と読み込む側の`SomeContext`は、`===`による比較で**厳密に同じオブジェクト**でなければなりません。


# 使い方
## コンポーネントツリーの深い下層にある子にデータを渡す
`useContext`フックは、コンポーネントのトップレベルで呼び出してください。読み込んだコンテクストの最新の値が受け取れます(subscribe)。

```react:src/Button.tsx
import { useContext } from 'react';
import { ThemeContext } from './App';

export const Button: FC<Props> = ({ children }) => {
	const theme = useContext(ThemeContext);
	const className = 'button-' + theme;
	return <button className={className}>{children}</button>;
};
```

`useContext`が返すのは、引数で渡したコンテクストに応じた最新の値です。Reactが**コンポーネントツリーを上層に向けて探し、もっとも近いコンテクストプロバイダの値を得ます**。したがって、コンテクストプロバイダは、値の使われる(`useContext`呼び出しする)子コンポーネントが含まれるツリー上層のいずれかの親をラップしなければならないのです。

```react:src/App.tsx
import { createContext } from 'react';
import { Form } from './Form';

const defaultTheme = 'dark';
export const ThemeContext = createContext(defaultTheme);
function App() {
	return (
		<ThemeContext.Provider value={defaultTheme}>
			<Form />
		</ThemeContext.Provider>
	);
}
```

上記コード例では、コンポーネント`Button`は`App`直下の子ではありません。けれど、プロパティ(`props`)とは異なり、コンテクストは子のツリー上であればコンポーネントが間に何階層加わっても構わないのです。

```react:src/Form.tsx
export const Form: FC = () => {
	return (
		<Panel title="Welcome">
			<Button>Sign up</Button>
			<Button>Log in</Button>
		</Panel>
	);
};
```

```react:src/Panel.tsx
import { useContext } from 'react';
import { ThemeContext } from './App';

export const Panel: FC<Props> = ({ title, children }) => {
	const theme = useContext(ThemeContext);
	const className = 'panel-' + theme;
	return (
		<section className={className}>
			<h1>{title}</h1>
			{children}
		</section>
	);
};
```

つぎのサンプル001では、コンポーネントは`App` > `Form` > `Panel` > `Button`というツリーになっています。それでも、`App`のモジュールから`createContext`でつくったコンテクスト(`ThemeContext`)の値は、`Button`コンポーネント内で`useContext`を呼び出して読み取れるのです。`theme`の値は`defaultTheme`ですので、`'dark'`に対応したCSS(`className`)が適用されました(なお、`Panel`も`theme`の値に応じて要素のスタイルが変わります)。

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

もっとも、サンプル001では、`theme`の値は`defaultTheme`(`'dark'`)の決め打ちです。このままでは、切り替えはできせん。

[注記] `useContext`は、つねに呼び出されたコンポーネントツリーの上層に向けてプロバイダを探します。したがって、**コンポーネント自身の戻り値(JSX)に加えられたプロバイダは対象となりません**。フックは必ずコンポーネントツリーの子から親に対して呼び出してください。

## コンテクストが渡すデータを更新する
コンテクストから渡すデータが決め打ちでは使えないでしょう。更新するには、値を親コンポーネントの状態変数にしてください。コンテクストのプロバイダに`value`値として渡すのはその状態変数です。

親コンポーネント`App`に、チェックボックス(`<input type="checkbox"`)を加えました。クリック(`onChange`)で切り替えるのが状態変数`theme`の値です。ツリー内の子コンポーネントは、すでにコンテキスト(`ThemeContext`)の`value`値が反映されるようになっています。

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

function App() {
	const [theme, setTheme] = useState(defaultTheme);
	return (
		// <ThemeContext.Provider value={defaultTheme}>
		<ThemeContext.Provider value={theme}>

			<label>
				<input
					type="checkbox"
					checked={theme === defaultTheme}
					onChange={({ target: { checked } }) => {
						setTheme(checked ? defaultTheme : 'light');
					}}
				/>
				Use dark mode
			</label>
		</ThemeContext.Provider>
	);
}
```

つぎのサンプル002で、チェックボックスによりコンポーネント(`Button`と`Panel`)のカラーが切り替わることをお確かめください。プロバイダに渡す`value`値が変わると、コンテクストを使っているすべての子コンポーネントは新たな値で再レンダーされるのです。

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

React公式サイトの「[「Updating data passed via context」](https://react.dev/reference/react/useContext#updating-data-passed-via-context)」には、簡単な説明とともにあと4つのサンプルが公開されています。これらは「[React + TypeScript: コンテクストが渡すデータを更新するコード例](https://qiita.com/FumioNonaka/items/8d71e3936cef0353327b)」に記事を改めて解説しましたので、ご参照ください。


## フォールバックされるコンテクストのデフォルト値を定める
`useContext()`が返すコンテクストの値は、プロバイダに渡された`value`で決まります。したがって、[`createContext`](https://ja.react.dev/reference/react/createContext)の引数を`null`にしても直ちには問題となりません。

```react:src/App.tsx
const ThemeContext = createContext(null);

function App() {
	const [theme, setTheme] = useState('dark');
	return (
		<ThemeContext.Provider value={theme}>

		</ThemeContext.Provider>
	);
}
```

ただし、ツリーを親コンポーネントに遡ってプロバイダが見つからない場合は、`useContext()`の戻り値は`createContext`の引数に渡したデフォルト値です。

つぎの`Button`コンポーネントのように誤ってプロバイダ(`<ThemeContext.Provider>`)のラップから外してしまうと、`useContext`の戻り値(`theme`)はデフォルト値`null`となってしまうでしょう。

```react:src/App.tsx
function App() {

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

			</ThemeContext.Provider>
			<Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
				Toggle theme
			</Button>

		</>
	);
}
```

```react:src/Button.tsx
export const Button: FC<Props> = ({ children, onClick }) => {
	const theme = useContext(ThemeContext);
	const className = 'button-' + theme;
	return (
		<button className={className} onClick={onClick}>
			{children}
		</button>
	);
};
```

`createContext`にデフォルト値を与えることで、問題が広がるのを抑えられます。コンテクストのデフォルト値は**あとから変えられません**。コンテクストの値は、前述のとおり状態変数と組み合わせて更新してください(サンプル003)。

```react:src/App.tsx
const defaultTheme = 'dark';
export const ThemeContext = createContext(defaultTheme);
function App() {
	const [theme, setTheme] = useState(defaultTheme);
	return (
		<ThemeContext.Provider value={theme}>

			<Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
				Toggle theme
			</Button>
		</ThemeContext.Provider>
	);
}
```

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

なお、TypeScriptを使っていれば、`createContext`のデフォルト値に`null`を渡すと、プロバイダから与える`value`の値に問題があると警告されるでしょう。初期値をあえて`null`に定めたいときは、`createContext`に型づけしてください。

```react
export const ThemeContext = createContext<string | null>(null);
```

## コンポーネントツリーの一部のコンテクストを上書きする
コンポーネントツリーの一部を上層とは`value`値が異なるコンテクストで包めば、下層の値は上書きできます。プロバイダの入れ子による値の上書きは、いくつ行っても構いません。

### コンテクストプロバイダで包まれた子コンポーネントの一部を別のプロバイダでラップする

つぎのアプリケーションは、コンテクストプロバイダに与えた値(`<ThemeContext.Provider value={defaultTheme}>`)に応じて、要素のカラー(CSS)を切り替えます(値は`'dark'`です)。

```react:src/App.tsx
const defaultTheme = 'dark';
export const ThemeContext = createContext(defaultTheme);
function App() {
	return (
		<ThemeContext.Provider value={defaultTheme}>
			<Form />
		</ThemeContext.Provider>
	);
}
```

親コンポーネントでコンテクストプロバイダに包まれた`Form`は、改めて戻り値のJSXの一部をプロバイダ(`<ThemeContext.Provider value="light">`)でラップしました。加えたのは、コンポーネント`Footer`で、与えた`value`は異なる値(`"light"`)です。

```react:src/Form.tsx
import { ThemeContext } from './App';

export const Form: FC = () => {
	return (
		<Panel title="Welcome">
			<Button>Sign up</Button>
			<Button>Log in</Button>
			<ThemeContext.Provider value="light">
				<Footer />
			</ThemeContext.Provider>
		</Panel>
	);
};
```

すると、コンポーネント`Footer`の中の[Settings]ボタン(`Button`)は、ツリー上層のもっとも近いプロバイダから`value`値(`"light"`)を得て、他のふたつのボタンとは違うカラー(CSS)が適用されます。サンプル004でお確かめください。ただし、ボタンにインタラクションは与えられていません。

```react:src/
export const Footer: FC = () => {
	return (
		<footer>
			<Button>Settings</Button>
		</footer>
	);
};
```

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

### 入れ子にしたコンテクストプロバイダで親の値から子に渡す値を定める
コンテクストプロバイダを入れ子にした場合、親から受け取った値に応じて子に渡す値が定められます。

つぎのモジュール`src/App.tsx`で入れ子になっているのは、コンポーネント`Section`です。

```react:src/App.tsx
const Headings = (count: number, title: string) =>
	Array.from(new Array(count), (_) => <Heading>{title}</Heading>);
function App() {
	return (
		<Section>
			<Heading>Title</Heading>
			<Section>
				{Headings(3, 'Heading')}
				<Section>
					{Headings(3, 'Sub-heading')}
					<Section>{Headings(3, 'Sub-sub-heading')}</Section>
				</Section>
			</Section>
		</Section>
	);
}
```

そして、`Section`コンポーネントが返すJSXは子(`children`)をコンテクストプロバイダ(`<LevelContext.Provider>`)で包んでいます。つまり、コンポーネントとともに、コンテクストプロバイダも入れ子になるということです。`value`として渡す値は、親コンテクストの値(`level`)に1加算しています。

```react:src/Section.tsx
export const Section: FC<Props> = ({ children }) => {
	const level = useContext(LevelContext);
	return (
		<section className="section">
			<LevelContext.Provider value={level + 1}>
				{children}
			</LevelContext.Provider>
		</section>
	);
};
```

コンテクストの初期値(`0`)に1ずつ加算して、入れ子のコンテクストプロバイダに`value`として渡るということです。

```react:src/LevelContext.ts
import { createContext } from 'react';

export const LevelContext = createContext(0);
```

コンポーネント`Section`に差し込まれた`Heading`は、`useContext`の値(`level`)に応じて、`<h1>`〜`<h6>`にテキスト(`children`)を加えて返します。

```react:src/Heading.tsx
export const Heading: FC<Props> = ({ children }) => {
	const level = useContext(LevelContext);
	switch (level) {
		case 0:
			throw Error('Heading must be inside a Section!');
		case 1:
			return <h1>{children}</h1>;
		case 2:
			return <h2>{children}</h2>;
		case 3:
			return <h3>{children}</h3>;
		case 4:
			return <h4>{children}</h4>;
		case 5:
			return <h5>{children}</h5>;
		case 6:
			return <h6>{children}</h6>;
		default:
			throw Error('Unknown level: ' + level);
	}
};
```

`<section>`で入れ子にされた見出し(`<Heading>`)は、`<h1>`から`<h6>`まで順に要素を切り替えて表示されるでしょう(サンプル005)。このコード例について段階を踏んだ詳しい解説は、「[React + TypeScript: コンテクストでデータを深い階層に渡す](https://qiita.com/FumioNonaka/items/babd1168d259416872ed)」をお読みください。

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

## オブジェクトや関数を渡すとき再レンダーを最適化する
コンテクストを用いて渡せるのは、オブジェクトや関数も含めた任意の値です。つぎのアプリケーションは、コンテクストプロバイダにふたつのプロパティが収められたオブジェクトを与えています。プロパティのひとつ(`login`)は関数です。

```react
const AuthContext = createContext(defaultContext);
function App() {
	const [currentUser, setCurrentUser] = useState('');
	const login = (response: Response) => {
		storeCredentials(response.credentials);
		setCurrentUser(response.user);
	};
	return (
		<AuthContext.Provider value={{ currentUser, login }}>
			<Page />
		</AuthContext.Provider>
	);
}
```

コンポーネント内に定められた関数は、再レンダーのたびに定義し直されることに気をつけなければなりません。再定義された関数は、中身が変わらなくとも、参照にもとづいて別と評価されます。すると、コンテクストの中の関数は定義が変わったとみなされ、`useContext`(`AuthContext`)を用いる子コンポーネントはすべて再レンダーされるのです。

アプリケーションがまだ小さければ、気にしなくても構いません。とはいえ、コンテクストの値としたオブジェクトのもうひとつのプロパティ(`currentUser`)も変わっていないなら、無駄に再レンダーしなくてもよいでしょう。

関数`login`を[`useCallback`](https://qiita.com/FumioNonaka/items/493ad3943d117c5ff1d6)で包めば、Reactは関数定義が実質的に変わったか確かめられます。さらに、コンテクストプロバイダの`value`に渡すオブジェクト(`contextValue`)をメモ化してしまえるのが[`useMemo`](https://qiita.com/FumioNonaka/items/86f6a91b98844beaff0d)です。このようにして、パフォーマンスが最適化できます。

```react
function App() {

	// const login = (response: Response) => {
	const login = useCallback((response: Response) => {
		storeCredentials(response.credentials);
		setCurrentUser(response.user);
		// };
	}, []);
	const contextValue = useMemo(
		() => ({
			currentUser,
			login,
		}),
		[currentUser, login]
	);
	return (
		// <AuthContext.Provider value={{ currentUser, login }}>
		<AuthContext.Provider value={contextValue}>

		</AuthContext.Provider>
	);
}
```

`login`に関数を返す`useCallback`に依存はありません(`[]`)。`contextValue`にオブジェクトを収める`useMemo`の依存値は、`currentUser`と`login`です。したがって、コンポーネント`App`は再レンダーされても、`useContext`(`AuthContext`)を呼び出している子コンポーネントは`currentUser`の値が変わらないかぎり再レンダーされません。

詳しくは、「[React + TypeScript: useCallbackフックの使い方と使いどころ](https://qiita.com/FumioNonaka/items/493ad3943d117c5ff1d6)」と「[React + TypeScript: useMemoフックの使い方と使いどころ](https://qiita.com/FumioNonaka/items/86f6a91b98844beaff0d)」をお読みください。


# トラブルへの対応
## コンテクストプロバイダに与えられている`value`値がコンポーネントから受け取れない
この問題でよくある理由はつぎの3つです。

1. `useContext`を呼び出すコンポーネントは、コンテクストプロバイダ(`<SomeContext.Provider>`)に包まれた子でなければなりません。
    - 自分のコンポーネントが返すJSXにレンダーしたコンテクストプロバイダの`value`値は得られません。
    - コンテクストプロバイダ(`<SomeContext.Provider>`)は、コンポーネントの外側、ツリーの上層に移してください。
2. コンポーネントがコンテクストプロバイダ(`<SomeContext.Provider>`)のラップから外れていることが考えられます。
    - [React Developer Tools](https://ja.react.dev/learn/react-developer-tools)を使ってツリー階層が正しいか確かめましょう。
3. コンテクスト(`SomeContext`)の参照が、プロバイダコンポーネントと利用側コンポーネントとの間で食い違っているかもしれません。
    - シンボリックリンクを使っていたり、ビルドツールの問題により、ふたつのブジェクトは異なって認識される場合があります。
    - ふたつの参照を`window.SomeContext1`や`window.SomeContext2`といったグローバル変数に割り当て、コンソールで`window.SomeContext1 === window.SomeContext2`が成り立つか確かめてください。
        - 等しくない場合は、ビルドツールのレベルで解決しなければなりません。


## コンテクストのデフォルト値を変えても得られる値がつねに`undefined`になる
`createContext`の引数に与えたデフォルト値は、コンポーネントツリーの上層にコンテクストプロバイダがない場合に返されます。したがって、つぎのような点をお確かめください。

* コンポーネントツリーの上層にコンテクストプロバイダはあって、`value`が与えられていないのかもしれません。
    - `value`がなければ、`value={undefined}`と同じです。

```react
// 🚩 NG: コンテクストプロバイダにvalueが与えられていない
<ThemeContext.Provider>
	<Button />
</ThemeContext.Provider>
```

* 子コンポーネントに`props`を渡すときと勘違いして、`value`以外の名前を使ってしまうことがあります。

```react
// 🚩 NG: コンテクストの値を渡すのはvalueです
<ThemeContext.Provider theme={theme}>
	<Button />
</ThemeContext.Provider>
```

ただ、どちらの場合もReactから警告が示されるでしょう。コンテクストプロバイダに値は`value`で渡してください。

```react
// ✅ OK: コンテクストの値はvalueで渡す
<ThemeContext.Provider value={theme}>
	<Button />
</ThemeContext.Provider>
```

前述のとおり、`createContext`の引数に与えたデフォルト値が返されるのは、コンポーネントツリーの上層に**コンテクストプロバイダがない場合**です。ツリー上層にコンテクストプロバイダ`<SomeContext.Provider value={someValue}>`はあり、`someValue`が`undefined`なら、コンポーネントから`useContext(SomeContext)`を呼び出して受け取るコンテクスト値は`undefined`になるでしょう。