react-router-domでLayoutとPrivateRouteを導入する
概要
react-router-dom を使って、ログイン時とログアウト時でページ遷移の許可を分ける、PrivateRoute
(おそらく、ProtectedRoute
)を実装する方法についてまとめます。
この方法を使えば、Layout コンポーネントの有無も切り替えられます。
ソースコードは以下になります。
実装方法
ディレクトリ構成
ディレクトリ構成は以下のようになります。
.
├── 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
はログイン状態を管理します。
LoginPage
、TopPage
、PrivatePage
はそれぞれページのコンポーネントです。
PublicRoute
、PrivateRoute
で公開、非公開のパスを切り替える処理を行います。
それぞれの解説を後述します。
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
を使ってログインユーザーの管理と、login
、logout
処理を実装します。
今回、ユーザー名はhoge@xxxx.com
、パスワードはhoge
固定です。
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
を付与するだけです。
ログイン状態にかかわらずコンポーネントを返します。
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
からログインユーザーを取得します。
ログインしている場合は、指定されたパスのルートを許可します。
ログインしていない場合は、ログイン画面へリダイレクトします。
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-dom
のRoute
と、自作したPublicRoute
に代入するコンポーネントは、ログインしなくても遷移可能です。PrivateRoute
に代入するコンポーンネントはログインしないと遷移できないことがわかります。
また、PublicRoute
とPrivateRoute
はNav
(ナビゲーションバー)が表示されますが、Route
には表示されません。
実際に起動して確認をします。
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.com
とhoge
を入力して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