【TypeScript × React】でグローバルStateを扱う方法

「ReactでPropsをバケツリレーするのは保守性が下がるから良くないよね」ということでグローバルStateを`useContext`を用いて実装するのですが、TypeScriptでの実装がやや特殊であったため、備忘録として残します。

ちなみに、本記事のソールコードは個人開発によるタスク管理アプリのものになります。

`useContext`によるグローバルStateの実装ですが、以下の手順で実装していきます。

①`React.createContext`でContextの器を作成する
②作成したContextの`Provider`でグローバルStateを扱いたいコンポーネントを囲う
③Stateを参照したいコンポーネントで`React.useContext`を使う

それでは早速、実装の解説に移ります。

# ①React.createContextでContextの器を作成する
まず最初にContextのプロバイダーコンポーネントを作成します。

```react:TaskListProvider.tsx
import {　createContext　} from "react";

export const TaskListContext =　createContext({});
```

これでContextの器は作成できました。
なお、`createContext()`の引数には初期値を設定することができます。

# ②作成したContextのProviderでグローバルStateを扱いたいコンポーネントを囲う

次にContextの値を参照できるようにするため、`Provider`を用いてContextの値を参照したいコンポーネント群を囲みます。（基本的にルートコンポーネントでOKです）


```react:TaskLictProvider.tsx
import {　
    createContext,
	FC,
	ReactNode,
	useState
} from "react";
import { Task } from "../../types/task";

type Props = {
	children: ReactNode;
};

export const TaskListContext =　createContext({});

export const TaskListProvider: FC<Props> = (props) => {
	const { children } = props;

	// タスクを配列で保持するState(初期値: 空の配列[])
	const [taskList, setTaskList] = useState<Task[]>([]);
　　　　　　　　// TaskListContextの中にProviderがあるため、childrenを囲む
    // valueにグローバルに扱う値を設定
	return (
		<TaskListContext.Provider value={{ taskList, setTaskList }}>
			{children}
		</TaskListContext.Provider>
	);
};
```

ここでのポイントは、Providerコンポーネントが何でも囲めるように**Propsとしてchildrenを受け取る**ようにするのがポイントです。
ちなみに、TypeScriptではchildrnの型を`ReactNode`とするのが良いそうです。
(ここでも、そのようにしています）

また、Providerコンポーネントは`value`というPropsを設定することができ、ここにグローバルに管理したい値を渡します。

ここまで完了したら、参照したい範囲のコンポーネントをProviderで囲みます。
以下では、アプリケーション全体で参照するようにしています。

```react:index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { App } from "./App";
import { TaskListProvider } from "./components/providers/TaskListProvider";

const root = ReactDOM.createRoot(
	document.getElementById("root") as HTMLElement
);
root.render(
	<React.StrictMode>
		<TaskListProvider>
			<App />
		</TaskListProvider>
	</React.StrictMode>
);
```
# ③Stateを参照したいコンポーネントでReact.useContextを使う
最後に参照したいコンポーネントでグローバルStateを参照します。

```react:TaskCard.tsx
import React, { FC, useState, useContext } from "react";
import { TaskAddInput } from "./input/TaskAddInput";
import { TaskCardDeleteButton } from "./button/TaskCardDeleteButton";
import { TaskCardTitle } from "./TaskCardTitle";
import { Tasks } from "./Tasks";
import { TaskListContext } from "../providers/TaskListProvider";
import styled from "styled-components";

/**
 * タスク一覧を表示するコンポーネント(親コンポーネント)
 *
 * @returns タスク一覧を構成する要素
 */
export const TaskCard: FC = () => {
	// タスク追加入力欄(input要素)を監視するState(初期値: "")
	const [inputText, setInputText] = useState<string>("");
　　　　　　　　// Contextから値を取得
	const { taskList, setTaskList } = useContext(TaskListContext);

	return (
		<STaskCard>
			<TaskCardTitle />
			<TaskCardDeleteButton />
			<TaskAddInput
				inputText={inputText}
				setInputText={setInputText}
				taskList={taskList}
				setTaskList={setTaskList}
			/>
			<Tasks taskList={taskList} />
		</STaskCard>
	);
};

const STaskCard = styled.div`
	width: 250px;
	padding: 10px 25px;
	margin: 10px 1%;
	background-color: rgb(228, 228, 228);
	border-radius: 5px;
`;
```

上記のようにContextを参照することができます。

ただ、このままだと以下のエラーが出ます。
```
Property "X" does not exist on type '{}'.
```
これは「プロパティ"X"は型"{}"に存在しません」というエラーです。
要するに、**"X"の型を定義すれば解決**できます。
つまり、"X"を定義した場所=グローバルStateの実装をおこなったコンポーネントで"X"の型を定義すればいいとう訳です。

ただ、そこが少し難しいところではありますが・・・

## エラーの解決法
さて、エラーの解決ですが以下の記述でうまくいきました。

```react:TaskListProvider.tsx
import React, {
	createContext,
	FC,
	ReactNode,
	useState,
	Dispatch,
	SetStateAction,
} from "react";
import { Task } from "../../types/task";

type Props = {
	children: ReactNode;
};

export const TaskListContext = createContext(
	{} as {
		taskList: Task[];
		setTaskList: Dispatch<SetStateAction<Task[]>>;
	}
);
export const TaskListProvider: FC<Props> = (props) => {
	const { children } = props;

	// タスクを配列で保持するState(初期値: 空の配列[])
	const [taskList, setTaskList] = useState<Task[]>([]);

	// TaskListContextの中にProviderがあるため、childrenを囲む
	// valueにグローバルに扱う値を設定
	return (
		<TaskListContext.Provider value={{ taskList, setTaskList }}>
			{children}
		</TaskListContext.Provider>
	);
};
```

`createContext()`の引数内で型を定義します。
`createContext({})`の`{}`内は初期値を渡すためのものであるため、ここで型定義を行うと値として認識され、結果、エラーとなります。

そのため、`as`をつけてas以降の`{}`内で型定義を行います。
このようにすることで、`Context`で型を定義でき、グローバルな参照が可能となります。

# 参考文献
> [・【TypeScript】useContextとuseStateを組み合わせて、子孫コンポーネントから直接先祖コンポーネントのstateを編集する](https://qiita.com/Rascal823/items/0f53ffbb410505b707f8)
> 
> [・React の props.children の型定義には ReactNode を使う](https://maku.blog/p/xenv4bh/)