React RouterはURLとコンポーネントを紐づけるライブラリです。React Routerを利用してパブリックルートとプライベートルートを作成する方法について解説します。
認証付きのReactアプリケーションを開発する場合、パブリックルートとプライベートルートが必要になる場合があります。まず、それらが何であるかを見ていきます。
Public Routes
パブリックルートは、アプリにサインイン前のみアクセスできるルートです。そのルートに該当するページコンポーネントは、例えばサインアップページ、パスワードを忘れた場合のページなどです。
Private Routes
プライベートルートは、アプリにサインイン後のみアクセスできるルートです。アプリによって様々ですが、そのルートに該当するページコンポーネントは、ユーザープロファイルページ、アプリ設定ページなどです。
パブリックルートとプライベートルートの制約は、サインイン前にプライベートルートにアクセスしてはならない、サインイン後にパブリックルートにアクセスしてはならないということです。
それでは、パブリックルートとプライベートルートを作成してみましょう。
Public Routes
まず、以下のようにパブリックルートの状態を処理するPublicRouteコンポーネントを作成しましょう。
import { memo, ReactNode, VFC } from 'react';
import { Route, Redirect, RouteProps } from 'react-router-dom';
type Props = {
children: ReactNode;
isAuthenticated: boolean;
redirectPath: string;
} & RouteProps;
export const PublicRoute: VFC<Props> = memo((props) => {
const { children, isAuthenticated, redirectPath, ...routeProps } = props;
return (
<Route
{...routeProps}
render={({ location }) => (
!isAuthenticated
? (children)
: (
<Redirect to={{
pathname: redirectPath,
state: { from: location }}
}/>
)
)}
/>
);
});
上記のコードでわかるように、パブリックルートコンポーネントは、children, isAuthenticated, redirectPath, ...routePropsのpropsを受け取ります。
ユーザーが認証済の場合、ユーザーはredirectPathにリダイレクトされ、未認証の場合のみパブリックルートにアクセスできます。
Private Routes
一方、プライベートルートコンポーネントはパブリックルートに似ていますが、リダイレクトの条件が違います。
ユーザーが未認証の場合、ユーザーはredirectPathにリダイレクトされ、認証済の場合のみ、プライベートルートにアクセスできます。
import { memo, ReactNode, VFC } from 'react';
import { Route, Redirect, RouteProps } from 'react-router-dom';
type Props = {
children: ReactNode;
isAuthenticated: boolean;
redirectPath: string;
} & RouteProps;
export const PrivateRoute: VFC<Props> = memo((props) => {
const { children, isAuthenticated, redirectPath, ...routeProps } = props;
return (
<Route
{...routeProps}
render={({ location }) => (
isAuthenticated
? (children)
: (
<Redirect to={{
pathname: redirectPath,
state: { from: location }}
}/>
)
)}
/>
);
});
Integrating Routes
最後に、以下のようにルートコンポーネントをRouterコンポーネントに統合しましょう。
import { memo, VFC } from "react";
import { Route, Switch, RouteProps } from "react-router-dom";
import { PageNotFound } from "components/pages/PageNotFound";
import { PageTop } from "components/pages/PageTop";
import { PublicRoute } from "router/PublicRoute"
import { PrivateRoute } from "router/PrivateRoute";
import { useGetToken } from "hooks/user/useGetToken";
import { PageSignIn } from "components/pages/PageSignIn";
import { PageSignUp } from "components/pages/PageSignUp";
import { PageForgot } from "components/pages/PageForgot";
import { PageTodo } from "components/pages/PageTodo";
import { PageSetting } from "components/pages/PageSetting";
import { PageHealthCheck } from "components/pages/PageHealthCheck";
import { TodosProvider } from "providers/TodosProvider";
import { PageResetPassword } from "components/pages/PageResetPassword";
export type PublicRouteProps = {
isAuthenticated: boolean;
redirectPath: string;
} & RouteProps;
export type PrivateRouteProps = {
isAuthenticated: boolean;
redirectPath: string;
} & RouteProps;
export const Router: VFC = memo(() => {
const { isAuthenticated } = useGetToken();
const defaultPublicRouteProps: PublicRouteProps = {
isAuthenticated: isAuthenticated,
redirectPath: '/todo',
};
const defaultPrivateRouteProps: PrivateRouteProps = {
isAuthenticated: isAuthenticated,
redirectPath: '/sign_in',
};
return (
<TodosProvider>
<Switch>
<PublicRoute
{...defaultPublicRouteProps}
exact
path="/"
sensitive
>
<PageTop />
</PublicRoute>
<PublicRoute
{...defaultPublicRouteProps}
exact
path="/sign_in"
sensitive
>
<PageSignIn />
</PublicRoute>
<PublicRoute
{...defaultPublicRouteProps}
exact
path="/sign_up"
sensitive
>
<PageSignUp />
</PublicRoute>
<PublicRoute
{...defaultPublicRouteProps}
exact
path="/user/forgot"
sensitive
>
<PageForgot />
</PublicRoute>
<PublicRoute
{...defaultPublicRouteProps}
exact
path="/user/reset_password"
sensitive
>
<PageResetPassword />
</PublicRoute>
<PrivateRoute
{...defaultPrivateRouteProps}
exact
path="/todo"
sensitive
>
<PageTodo />
</PrivateRoute>
<PrivateRoute
{...defaultPrivateRouteProps}
exact
path="/user/settings"
sensitive
>
<PageSetting />
</PrivateRoute>
<Route path="*">
<PageNotFound />
</Route>
</Switch>
</TodosProvider>
);
});
認証ステータスisAuthenticatedはuseGetToken()関数から受け取ります。
パブリックルートのリダイレクトパスを/todo、プライベートルートのリダイレクトパスを/sign_inに設定しました。
未認証ルートをPublicRouteコンポーネントでラップし、認証済ルートをPrivateRouteコンポーネントでラップしました。
これで、パブリックルートとプライベートルートが構成されました。一致するものがない場合、PageNotFoundがレンダリングされます。
まとめ
Routerコンポーネントはアプリで使用するURLとそのURLに紐ずくページコンポーネントが全て記述されています。また認証によるリダイレクト先のURLも記述されています。メンテナンス性の高いコードを実現しています。
以上、お役に立てれば幸いです。
ソースコードをGitHubへアップしました。今回解説した内容はGitHubへアップしたSingle Page Applicationの一部分です。
参考文献
この記事は以下の情報を参考にして執筆しました。