😀

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

2022/05/22に公開

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

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

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

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

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

①React.createContextでContextの器を作成する

まず最初にContextのプロバイダーコンポーネントを作成します。

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

export const TaskListContext = createContext({});

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

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

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

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で囲みます。
以下では、アプリケーション全体で参照するようにしています。

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を参照します。

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"の型を定義すればいいとう訳です。

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

エラーの解決法

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

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を編集する

・React の props.children の型定義には ReactNode を使う

Discussion