見出し画像

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に助けてもらいました。
便利で助かりますね・・・!

《この公式ブロガーのおすすめ記事》


執筆者プロフィール: 渡部 瑠菜
2022年4月に新卒入社後、ITソリューション部に所属。
AWSやReactを勉強中。
趣味は絵を描くことや2D/3Dモデリングなど。

お問合せはお気軽に
SHIFTについて(コーポレートサイト)
SHIFTのサービスについて(サービスサイト)
SHIFTの導入事例
お役立ち資料はこちら
SHIFTの採用情報はこちら   

PHOTO:UnsplashPhilipp Katzenberger