react-router-domでLayoutとPrivateRouteを導入する

2021/11/17に公開

概要

react-router-dom を使って、ログイン時とログアウト時でページ遷移の許可を分ける、PrivateRoute(おそらく、ProtectedRoute)を実装する方法についてまとめます。
この方法を使えば、Layout コンポーネントの有無も切り替えられます。
ソースコードは以下になります。
https://github.com/Msksgm/react-private-route-example

実装方法

ディレクトリ構成

ディレクトリ構成は以下のようになります。

.
├── README.md
├── package.json
├── public
├── src
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── lib
│   │   ├── Auth.tsx
│   │   ├── PrivateRoute.tsx
│   │   └── PublicRoute.tsx
│   ├── organisms
│   │   └── Nav.tsx
│   ├── pages
│   │   ├── LoginPage.tsx
│   │   ├── PrivatePage.tsx
│   │   └── TopPage.tsx
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   └── setupTests.ts
├── tsconfig.json
└── yarn.lock

./src/App.tsx

App.tsxではreact-router-domを用いてルーティングの設定をします。

AuthProviderはログイン状態を管理します。
LoginPageTopPagePrivatePageはそれぞれページのコンポーネントです。
PublicRoutePrivateRouteで公開、非公開のパスを切り替える処理を行います。

それぞれの解説を後述します。

./src/App.ts
import { FC } from "react";
import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";

import LoginPage from "./pages/LoginPage";
import TopPage from "./pages/TopPage";
import PrivatePage from "./pages/PrivatePage";

import { AuthProvider } from "./lib/Auth";
import PublicRoute from "./lib/PublicRoute";
import PrivateRoute from "./lib/PrivateRoute";

const App: FC = () => {
  return (
    <>
      <AuthProvider>
        <BrowserRouter>
          <Switch>
            <Route exact path="/login" component={LoginPage} />
            <PublicRoute exact path="/top" component={TopPage} />
            <PrivateRoute exact path="/private" component={PrivatePage} />
            <Redirect to="/login" />
          </Switch>
        </BrowserRouter>
      </AuthProvider>
    </>
  );
};

export default App;

./lib/Auth.tsx

Auth.tsxでは、createContextを使ってログインユーザーの管理と、loginlogout処理を実装します。
今回、ユーザー名はhoge@xxxx.com、パスワードはhoge固定です。

./lib/Auth.tsx
import { createContext, useState, useEffect } from "react";

type CurrentUser = {
  userName: string | null | undefined;
};

type AuthMethods = {
  login(username: string, password: string): void;
  logout(): void;
};

type LoginStatus = {
  loading: boolean;
};

type Context = { currentUser: CurrentUser | null | undefined } & AuthMethods &
  LoginStatus;

const AuthContext = createContext<Context>({
  currentUser: undefined,
  login(username: string, password: string): void {
    throw Error("Not Implemented!");
  },
  logout(): void {
    throw Error("Not Implemented!");
  },
  loading: false,
});

const AuthProvider = (props: any) => {
  const [currentUser, setCurrentUser] = useState<CurrentUser | undefined>(
    undefined
  );
  const [loading, setLoading] = useState<boolean>(true);

  const login = async (username: string, password: string) => {
    if (username === "hoge@xxxx.com" && password === "hoge") {
      setCurrentUser({ userName: username });
      localStorage.setItem("token", username);
      setLoading(false);
    } else {
      throw Error("password または usernameが違います");
    }
  };

  const logout = () => {
    localStorage.removeItem("token");
    setCurrentUser(undefined);
  };

  const isSignedIn = async () => {
    if (localStorage.getItem("token")) {
      try {
        setCurrentUser({ userName: localStorage.getItem("token") });
      } catch (err) {
        await setCurrentUser(undefined);
        localStorage.removeItem("token");
      }
    }
    setLoading(false);
  };

  useEffect(() => {
    isSignedIn();
  }, [setCurrentUser]);

  return (
    <>
      <AuthContext.Provider
        value={{
          currentUser: currentUser,
          login: login,
          logout: logout,
          loading: loading,
        }}
      >
        {props.children}
      </AuthContext.Provider>
    </>
  );
};

export { AuthContext, AuthProvider };

./lib/PublicRoute.tsx

PublicRouteでは、レイアウトコンポーネントであるNavを付与するだけです。
ログイン状態にかかわらずコンポーネントを返します。

./lib/PublicRoute.tsx
import { FC } from "react";
import { Route, RouteProps } from "react-router-dom";

import Nav from "../organisms/Nav";

const PublicRoute: FC<RouteProps> = ({ component, ...rest }) => {
  return (
    <>
      <Nav />
      <Route component={component} {...rest} />;
    </>
  );
};

export default PublicRoute;

./lib/PrivateRoute.tsx

PrivateRouteでは、AuthContextからログインユーザーを取得します。
ログインしている場合は、指定されたパスのルートを許可します。
ログインしていない場合は、ログイン画面へリダイレクトします。

./lib/PrivateRoute.tsx
import { FC, useContext } from "react";
import { Route, Redirect, RouteProps } from "react-router-dom";
import { AuthContext } from "./Auth";

import Nav from "../organisms/Nav";

const PrivateRoute: FC<RouteProps> = ({ component, ...rest }) => {
  const { currentUser, loading } = useContext(AuthContext);
  if (!loading) {
    if (currentUser) {
      return (
        <>
          <Nav />
          <Route component={component} {...rest} />;
        </>
      );
    } else {
      return <Redirect to="/login" />;
    }
  } else {
    return <></>;
  }
};

export default PrivateRoute;

./pages 配下のコンポーネント

LoginPage.tsxではログイン処理を実装しています。

TopPage.tsxでは、ログイン状態によってユーザーの表示の有無を切替て表示します。

PrivatePage.tsxでは、<h1>Private page</h1>を表示するためだけのコンポーネントです。

動作確認の方がわかりやすいのでソースコードは省略します。

./organisms/Nav.tsx

ナビゲーションバーを実装しています。ログイン状態によって表示がかわります。

こちらも、動作確認の方がわかりやすいのでソースコードは省略します。

動作確認

App.tsx の再掲

あらためてApp.tsxを確認します。

先ほどの実装から、react-router-domRouteと、自作したPublicRouteに代入するコンポーネントは、ログインしなくても遷移可能です。PrivateRouteに代入するコンポーンネントはログインしないと遷移できないことがわかります。

また、PublicRoutePrivateRouteNav(ナビゲーションバー)が表示されますが、Routeには表示されません。

実際に起動して確認をします。

./src/App.tsx
const App: FC = () => {
  return (
    <>
      <AuthProvider>
        <BrowserRouter>
          <Switch>
            <Route exact path="/login" component={LoginPage} />
            <PublicRoute exact path="/top" component={TopPage} />
            <PrivateRoute exact path="/private" component={PrivatePage} />
            <Redirect to="/login" />
          </Switch>
        </BrowserRouter>
      </AuthProvider>
    </>
  );
};

起動

git clone https://github.com/Msksgm/react-private-route-example.git
cd react-private-route-example
yarn
yarn start

画面確認

起動すると、localhost:3000/loginに遷移します。
ナビーゲションバーが表示されていません。
TOP(NOT LOGIN)を押下します。

ログイン画面
ログイン画面

localhost:3000/topに遷移します。
you are not Loginが表示されています。
ナビゲーションバーが表示されています。
ナビゲーションバーのLOGINを押下して、localhost:3000/loginに戻ります。

トップ画面
トップ画面

今度は、hoge@xxxx.comhogeを入力してLOG INを押下します。

ログイン画面
ログイン画面

localhost:3000/topに遷移して、username が表示されていることがわかります。
またナビゲーションバーにPrivateが表示されています。
Privateを押下します。

トップ画面
トップ画面

localhost:3000/privateに遷移します。
Private Pageが表示されています。
右上のLOGOUTからログアウトします。

プライベート画面
プライベート画面

トップ画面から直接 url にlocalhost:3000/privateを入力しても、localhost:3000/loginにリダイレクトされます。
直接 url でlocalhost:3000/topに入力したら遷移します。
このようにしてルーティングの保護ができるようになりました。

トップ画面
トップ画面

まとめ

react-router-domを用いて、ルーティングを保護する方法やレイアウトコンポーネントの共通化について解説しました。
この方法を応用すれば、管理者権限によるルーティングの保護や、ページごとのレイアウトの切り替えが可能になります。

Discussion