React useContextでグローバルstateを扱う
はじめに
こんにちは、株式会社SHIFT ITソリューション部の渡部です。
React、TypeScriptを使用した SPA開発と Jestによる自動テストを行っています。
今回は複数のコンポーネントで値を使用するために、Contextを使用してグローバルstateを作成します。
各コンポーネントで書き換え可能にしたり、テストをしたり、というところでかなり躓いてしまったのでその備忘録となります。皆様の参考になれば幸いです。
環境
Vite : 4.3.9
React : 18.2.8
TypeScript : 5.1.3
Jest : 29.5.0
React Router dom : 6.12.0
実装
例として、ユーザー情報の表示や登録をする機能を作成します。
今回はユーザーの名前、メールアドレスをグローバルstateとして管理します。
以下でサンプルを動かすことができます。
https://codesandbox.io/p/sandbox/cranky-tree-88yxqn?file=%2Fsrc%2FeditUser.tsx%3A1%2C1
context.tsx
createContext関数でContextを作成し、Providerをコンポーネント化します。
初期値はnullを設定しています。
import { createContext, useState } from "react";
type Props = {
children: JSX.Element;
};
type UserData = {
name: string;
mailAddress: string;
};
type UserContextType = {
user: UserData | null;
setUser: (user: UserData) => void;
};
//contextを作成
export const UserContext = createContext<UserContextType>({
user: null,
setUser: (user) => {},
});
// Providerをコンポーネント化する
export default function Context({ children }: Props) {
//Stateの設定 初期値を設定
const [user, setUser] = useState<UserData | null>(null);
// valueを設定してProviderコンポーネントを返す
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
App.tsx
Providerコンポーネントでアクセスできる範囲を指定します。
今回は全てのコンポーネントでContextが使用できるように全体を指定しました。
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Top from "./top";
import EditUser from "./editUser";
import Header from "./header";
import Context from "./context";
function App() {
return (
<Context>
<BrowserRouter>
<Header />
<Routes>
<Route path={`/`} element={<Top />} />
<Route path={`/edit`} element={<EditUser />} />
</Routes>
</BrowserRouter>
</Context>
);
}
export default App;
top.tsx
トップページの実装です。
この画面ではContextの値の参照、表示を行います。
edit を押下するとContextの編集ページへ遷移します。
import { useContext } from "react";
import { Link } from "react-router-dom";
import { UserContext } from "./context";
import "./App.css";
function Top() {
//Contextの参照
const { user } = useContext(UserContext);
return (
<div className="App">
<span>name : {user ? user.name : "未入力"}</span>
<span>mail : {user ? user.mailAddress : "未入力"}</span>
<Link to={"/edit"}>edit</Link>
</div>
);
}
export default Top;
editUser.tsx
Contextの値を参照、更新をする画面です。
import { useContext } from "react";
import { useForm, SubmitHandler } from "react-hook-form";
import { useNavigate, Link } from "react-router-dom";
import { UserContext } from "./context";
import "./App.css";
type Inputs = {
name: string;
mail: string;
};
function EditUser() {
const navigate = useNavigate();
//Contextの参照 setUserで更新
const { user, setUser } = useContext(UserContext);
const { register, handleSubmit } = useForm<Inputs>({
mode: "onSubmit",
});
const onSubmit: SubmitHandler<Inputs> = (data) => {
//Contextの更新
setUser({ name: data.name, mailAddress: data.mail });
navigate("/");
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="App">
<div className="row">
<label>name</label>
<input defaultValue={user?.name} {...register("name")} />
</div>
<div className="row">
<label>mail</label>
<input defaultValue={user?.mailAddress} {...register("mail")} />
</div>
<button>submit</button>
<Link to={"/"}>back</Link>
</div>
</form>
</div>
);
}
export default EditUser;
Jest
こちらもなかなか書き方が分からず苦労したのですが、以下のように書くとContextの値をモックしてテストができました。
import { render, screen } from '@testing-library/react';
import Top from './top'
import { UserContext } from './context';
describe('top test', () => {
test('test', async () => {
const mockContextValue = {
user: {name: '山田 太郎', mailAddress: 'yamada@mail.com'},
setUser: jest.fn(),
};
render(
<UserContext.Provider value={mockContextValue}>
<Top />
</UserContext.Provider>
)
expect(screen.getByText('山田 太郎')).toBeInTheDocument();
});
})
終わりに
実装は少し大変でしたが、とても便利です。 リロードすると初期値に戻ってしまうので、そこだけ注意です。LocalStrageを使うことや、リロードを検知するとAPIを呼び出すなどの対策が必要になります。
今回のテスト部分は実はChatGPTに助けてもらいました。
便利で助かりますね・・・!
《この公式ブロガーのおすすめ記事》
お問合せはお気軽に
SHIFTについて(コーポレートサイト)
SHIFTのサービスについて(サービスサイト)
SHIFTの導入事例
お役立ち資料はこちら
SHIFTの採用情報はこちら
PHOTO:UnsplashのPhilipp Katzenberger