「ReactでPropsをバケツリレーするのは保守性が下がるから良くないよね」ということでグローバルStateをuseContext
を用いて実装するのですが、TypeScriptでの実装がやや特殊であったため、備忘録として残します。
ちなみに、本記事のソールコードは個人開発によるタスク管理アプリのものになります。
useContext
によるグローバルStateの実装ですが、以下の手順で実装していきます。
①React.createContext
でContextの器を作成する
②作成したContextのProvider
でグローバルStateを扱いたいコンポーネントを囲う
③Stateを参照したいコンポーネントでReact.useContext
を使う
それでは早速、実装の解説に移ります。
①React.createContextでContextの器を作成する
まず最初にContextのプロバイダーコンポーネントを作成します。
import { createContext } from "react";
export const TaskListContext = createContext({});
これでContextの器は作成できました。
なお、createContext()
の引数には初期値を設定することができます。
②作成したContextのProviderでグローバルStateを扱いたいコンポーネントを囲う
次にContextの値を参照できるようにするため、Provider
を用いてContextの値を参照したいコンポーネント群を囲みます。(基本的にルートコンポーネントでOKです)
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で囲みます。
以下では、アプリケーション全体で参照するようにしています。
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を参照します。
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"の型を定義すればいいとう訳です。
ただ、そこが少し難しいところではありますが・・・
エラーの解決法
さて、エラーの解決ですが以下の記述でうまくいきました。
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を編集する