8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

「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 を使う

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?