React + TypeScript: useContextで子コンポーネントが親からvalueを受け取るときの注意

「[コンテクスト](https://ja.reactjs.org/docs/context.html)」は、コンポーネントツリーの中でデータを共有する仕組みです(「[コンテクストをつくってuseContextフックで参照する](https://fumiononaka.com/Business/html5/FN2101005.html#sec01)」参照)。そして、`useContext`フックは「コンテクストオブジェクト（`React.createContext`からの戻り値）を受け取り、そのコンテクストの現在値を返します。」(「[`useContext`](https://ja.reactjs.org/docs/hooks-reference.html#usecontext)」)。とくに便利なのは、データをまとめたうえで、コンポーネントからロジックを切り出して(別モジュールにして)しまえることです。

ところが、コンテクストのロジックがコンポーネントから呼び出せないという問題に遭遇してしまいました。`useContext`の使い方について、理解の浅い点があったためです。備忘録としてまとめます。


# `useContext`を使わずルートモジュールにまとめたテスト用コード
つぎに掲げるのが、コンテクストを使う前のテスト用コードです(ルートモジュール`src/App.tsx`)。ご覧のとおりとても簡素で、ボタン(`<button>`要素)をクリックすると(`onClick`ハンドラ)コールバック(`test()`)が呼ばれて、コンソールにテキストが出力されます。今回のコードは、TypeScriptを用いるものの、まだ型づけされていません。型推論で済んでしまっているからです。

```react:src/App.tsx
import React, { useCallback } from "react";

function App() {
	const test = useCallback(() => {
		console.log("test button clicked");
	}, []);
	return (
		<div>
			<button type="button" onClick={test}>
				test
			</button>
		</div>
	);
}

export default App;
```


# コンテクストのモジュールをつくる
新たなコンテクストのモジュール(`src/TestContext.tsx`)にロジックを切り出します。コンテクストをつくるのは、[`createContext()`](https://ja.reactjs.org/docs/context.html#reactcreatecontext)です(「[コンテクストをつくってuseContextフックで参照する](https://fumiononaka.com/Business/html5/FN2101005.html#sec01)」参照)。以下のコード001のとおり、引数にはコンテクストの初期値を与え、戻り値のコンテクスト(`TestContext`)は`export`してください(「[Create React App 入門 06: アプリケーションのロジックをコンテクストに切り出す](https://fumiononaka.com/Business/html5/FN2101006.html)」参照)。

このとき、鍵になることはふたつあります。ひとつは、`export`するコンポーネント(`TestContextProvider`)がコンテクストの`Provider`をJSXとして返すことです。ルートモジュール(`src/App.tsx`)は、後述のとおり`import`した`Provider`のコンポーネントでツリーを包みます。

もうひとつの鍵は、`Provider`のコンポーネント(`TestContextProvider`)が`props.children`を引数に受け取り、JSXの`Provider`でラップすることです(「[ラップしたコンポーネントツリーをprops.childrenで受け取る](https://fumiononaka.com/Business/html5/FN2101006.html#sec02)」参照)。

#### コード001■コンテクストのモジュール
```react:src/TestContext.tsx
import { createContext, useCallback } from "react";

type TestContextType = {
	test: () => void;
};
type Props = {
	children: React.ReactNode;
};
export const TestContext = createContext<TestContextType>({
	test: () => undefined,
});
export const TestContextProvider: React.VFC<Props> = ({ children }) => {
	const test = useCallback(() => {
		console.log("test button clicked");
	}, []);
	return (
		<TestContext.Provider value={{ test }}>{children}</TestContext.Provider>
	);
};
```


# `useContex`の誤った使い方
これで、ルートモジュール(`src/App.tsx`)はコンテクストのモジュール(`src/TestContext.tsx`)から`TestContext`と`TestContextProvider`が`import`でき、`Provider`で包んだコンポーネントツリーは前掲コード001で`value`に与えられたデータ(`test`)が得られるようになりました。参照するには、コンテクストを引数にした`useContext`の戻り値からプロパティを取り出すだけです。

```react:src/App.tsx
// import React, { useCallback } from "react";
import React, { useContext } from "react";
import { TestContext, TestContextProvider } from "./TestContext";

function App() {
	/* const test = useCallback(() => {
		console.log("test button clicked");
	}, []); */
	const { test } = useContext(TestContext);
	return (
		<TestContextProvider>
			<div>
				<button type="button" onClick={test}>
					test
				</button>
				<div>test: {String(test)}</div>
			</div>
		</TestContextProvider>
	);
}
```

`Provider`で包まれたツリー内のコンポーネントは、階層を上にたどって`value`に与えられたプロパティの参照が得られます。ところが、ボタン(`<button>`要素)をクリックしても、`onClick`に定めたコンテクスト(`TestContext`)のコールバック関数(`test`)が呼び出せません。

そこで上記コードは、確認のためボタンの下に加えた`<div>`要素に、取り出したはずのコールバック関数(`test`)を文字列で表示してみました。すると、関数としては認識されています。ただし、つぎのように値が初期値のまま、コンテクストに定めた関数に改められていないようです。

```terminal
test: () => undefined
```

公式ドキュメントを読むと、つぎのように書かれていました。コンポーネントの「関数に渡される引数`value`は、ツリー内の上位で一番近いこのコンテクスト用のプロバイダの`value`プロパティと等しくなります。このコンテクスト用のプロバイダが上位に存在しない場合、引数の`value`は`createContext()`から渡された`defaultValue`と等しくなります」(「[Context.Consumer](https://ja.reactjs.org/docs/context.html#contextconsumer)」)。つまり、コンポーネントツリーの上層に`Provider`がないということのようです。



# `useContext`は子コンポーネントの中から呼び出す
改めて公式ドキュメントの「[useContext](https://ja.reactjs.org/docs/hooks-reference.html#usecontext)」を読むとつぎのように説明されています。見逃してならないのは、「ツリー内でこのフックを呼んだコンポーネント」というくだりです。もちろん、子コンポーネントはJSXで`Provider`に包まれていなければなりません。それだけでなく、`useContext`は子コンポーネントの中から呼び出さなければならないのです。

> コンテクストの現在値は、ツリー内でこのフックを呼んだコンポーネントの直近にある`<MyContext.Provider>`の`value`の値によって決定されます。

> 直近の`<MyContext.Provider>`が更新された場合、このフックはその`MyContext`プロバイダに渡された最新の`value`の値を使って再レンダーを発生させます。

前掲の誤ったコードでは、`useContext`を親のルートコンポーネント(`App`)から呼び出しました。すると、このコンポーネントから上層に`Provider`を探してしまうのです。ですから、見当たらず、初期値のままになったということでしょう。

解決するには、子コンポーネントを分けます。モジュールはそのままでも、つぎのように子コンポーネント(`TestButton`)を新たに定め、`useContext`はその中から呼び出せばよいのです。これで、コンテクストのコンポーネント(`TestContextProvider`)に定めた関数(`test`)が`value`のプロパティとして取り出せます。

```react:src/App.tsx
const TestButton: React.VFC = () => {
	const { test } = useContext(TestContext);
	return (
		<div>
			<button type="button" onClick={test}>
				test
			</button>
		</div>
	);
};
function App() {
	// const { test } = useContext(TestContext);
	return (
		<TestContextProvider>
			{/* <div>
				<button type="button" onClick={test}>
					test
				</button>
				<div>test: {String(test)}</div>
			</div> */}
			<TestButton />
		</TestContextProvider>
	);
}
```

書き直したルートモジュール(`src/App.tsx`)の記述全体を、つぎのコード002にまとめます。

#### コード002■`useContext`は子コンポーネントの中から呼び出す
```react:src/App.tsx
import React, { useContext } from "react";
import { TestContext, TestContextProvider } from "./TestContext";

const TestButton: React.VFC = () => {
	const { test } = useContext(TestContext);
	return (
		<div>
			<button type="button" onClick={test}>
				test
			</button>
		</div>
	);
};
function App() {
	return (
		<TestContextProvider>
			<TestButton />
		</TestContextProvider>
	);
}

export default App;
```

なにしろエラーが出ないので、問題の絞り込みに手間取りました。また、何が起こっているのかわからないままでは、検索のしようもありません。もっとも、ルートモジュールから`useContext`を使いたいということも稀でしょう。それもあってか、公式ドキュメントの記述もあっさりしています。今回は、同じ問題につまずいた方のために記事にしました。