ReactのContext

React内でグローバル変数的にアクセスできるContextを使い、ログインしたあとでそのユーザ名などを保持する機能を実現したかった。 ちなみにContextとは別にcookieなどにアクセストークンを管理し、本当に大事なデータはアクセストークンを使わないと取得できないようにすることでセキュリティは担保すればよいと思っている。

以下でプロジェクトを作った直後のところからスタート。

npx create-react-app 云々 --template typescript

いきなり一番重いところから。App.tsxを以下のように書き換える。

import React, { createContext, useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

import { Header } from "./Header";
import { Login } from "./Login";
import { Logout } from "./Logout";
import { Contents } from "./Contents";
import { Admin } from "./Admin";
import { Portal } from "./Portal";

//
// コンテキストで管理したいデータの型を定義する。
//
export type UserInfo = {
  user_name: string;
  mail_address: string;
  admin_flag: number;
};

//
// コンテキストを作る。初期値も与える。
//
export const UserContext = createContext({
  user_name: "初期値",
  mail_address: "初期値",
  admin_flag: 0,
});

function App() {
  //
  // コンテキスト上に保持する変数とそれを変更する関数の宣言
  //
  const [user_info, setUserInfo] = useState<UserInfo>({
    user_name: "",
    mail_address: "",
    admin_flag: -1,
  });

  // コンテキストを参照したいコンポーネントたちを
  //  コンテキスト.Providerでくくる。
  // LoginとLogoffにはコンテキストの値を変更するための
  // 関数を渡す。それ以外はそれぞれのコンポーネント内で
  // コンテキストにアクセスしにいく。
  return (
    <>
      <UserContext.Provider value={user_info}>
        <Header />
        <BrowserRouter>
          <Routes>
            <Route
              path="/login"
              element={<Login setUserInfo={setUserInfo} />}
            />
            <Route
              path="/logout"
              element={<Logout setUserInfo={setUserInfo} />}
            />
            <Route path="/contents" element={<Contents />} />
            <Route path="/admin" element={<Admin />} />
            <Route path="/" element={<Portal />} />
          </Routes>
        </BrowserRouter>
      </UserContext.Provider>
    </>
  );
}

export default App;

ログイン用のコンポーネントをLogin.tsxというファイルのなかに以下のように書いて実装。 本当のログイン処理はもうちょっと真面目に書く必要あり。

import { useState } from "react";
import { Link } from "react-router-dom";
import { UserInfo } from "./App";

//
// ユーザ情報を変更する関数を受け取るためのインタフェース
//
interface LoginProps {
  setUserInfo: (userInfo: UserInfo) => void;
}

export const Login = (props: LoginProps) => {
  const [username, setUsername] = useState<string>("");
  const [password, setPassword] = useState<string>("");

  const procLogin = () => {
    // ログインを処理を行う。
    // この実装はContextの使い方の勉強用なので省略し、
    // 代わりにダミーの値を入れる。
    console.log(username, password);
    if (username === "user01" && password === "user01") {
      // コンテキストを書き換える
      props.setUserInfo({
        user_name: "大谷翔平",
        mail_address: "shohei.otani@example.com",
        admin_flag: 0,
      });
    } else if (username === "admin" && password === "admin") {
      // コンテキストを書き換える
      props.setUserInfo({
        user_name: "長嶋茂雄",
        mail_address: "nagashima@example.com",
        admin_flag: 1,
      });
    }
  };

  return (
    <>
      <p>
        username :
        <input type="text" value={username}
            onChange={(e)=>{setUsername(e.target.value);}} />
      </p>
      <p>
        password :
        <input type="password" value={password}
            onChange={(e)=>{ setPassword(e.target.value); }} />
      </p>
      <p>
        <button onClick={(e) => { procLogin(); }}>login</button>
      </p>
      <hr />
      <Link to="/">ポータルへ</Link>
    </>
  );
};

ログアウトでcontextの中身を初期化する。 Logout.tsxというファイルを作って以下のように書く。

import { Link } from "react-router-dom";
import { UserInfo } from "./App";

//
// ユーザ情報を変更する関数を受け取るためのインタフェース
//
interface LogoutProps {
  setUserInfo: (userInfo: UserInfo) => void;
}

export const Logout = (props: LogoutProps) => {
  return (
    <>
      <button
        onClick={(e) => {
          props.setUserInfo({
            user_name: "",
            mail_address: "",
            admin_flag: -1,
          });
        }}
      >
        ログオフする
      </button>
      <Link to="/">ポータルへ</Link>
    </>
  );
};

どのURLにアクセスしてもページの上のほうにログインしているユーザの名前などが表示される領域を作る。 Header.tsxという名前のファイルを作り以下のように書く。

import { useContext } from "react";
import { UserContext } from "./App";

//
// 各ページの先頭に表示する、ログイン中のユーザ名とそのメアドを表示するイメージ
//
export const Header = () => {
  const user = useContext(UserContext);
  return (
    <>
      [ {user.user_name} | {user.mail_address} ] <br />
    </>
  );
};

ポータル画面では、ログインしているかどうか、またログインしている場合はそのユーザの権限次第で表示するリンクを切り替える。 Portal.tsxというファイルを作って以下のように書く。

import { useContext } from "react";
import { Link } from "react-router-dom";
import { UserContext } from "./App";

export const Portal = () => {
  const user = useContext(UserContext);
  // 権限ごとにリンクを変更する。
  // (アクセス制御は直リンも考慮しリンク先で実施)
  return (
    <>
      <h1>ポータル</h1>
      {user.admin_flag === 1 ? (
        <>
          <Link to="/admin">管理ページ</Link> |
        </>
      ) : (
        ""
      )}
      {user.admin_flag === -1 ? (
        <Link to="/login">ログイン</Link>
      ) : (
        <>
          <Link to="/contents">コンテンツ</Link> |
          <Link to="/logout">ログアウト</Link>
        </>
      )}
    </>
  );
};

あとは、適当にコンテンツを作る。 Contents.tsxという名前のファイルを作って以下のようなかんじで書く。

import { Link } from "react-router-dom";

export const Contents = () => {
  return (
    <>
      <h1>コンテンツ</h1>
      ログインしていればみることができます。
      ただこの実装だと直接URLをたたくとログインしていなくても見えてしまうので、
      実際の実装ではAPIでとってくるなどしてアクセスコントロールすること。
      <hr />
      <Link to="/">ポータルに戻る</Link>
    </>
  );
};

以上。