React Router in TypeScriptを利用してパブリックルートとプライベートルートを作成する方法

React RouterはURLとコンポーネントを紐づけるライブラリです。React Routerを利用してパブリックルートとプライベートルートを作成する方法について解説します。

認証付きのReactアプリケーションを開発する場合、パブリックルートとプライベートルートが必要になる場合があります。まず、それらが何であるかを見ていきます。


## Public Routes

パブリックルートは、アプリにサインイン前のみアクセスできるルートです。そのルートに該当するページコンポーネントは、例えばサインアップページ、パスワードを忘れた場合のページなどです。


## Private Routes

プライベートルートは、アプリにサインイン後のみアクセスできるルートです。アプリによって様々ですが、そのルートに該当するページコンポーネントは、ユーザープロファイルページ、アプリ設定ページなどです。

パブリックルートとプライベートルートの制約は、サインイン前にプライベートルートにアクセスしてはならない、サインイン後にパブリックルートにアクセスしてはならないということです。

それでは、パブリックルートとプライベートルートを作成してみましょう。


## Public Routes

まず、以下のようにパブリックルートの状態を処理するPublicRouteコンポーネントを作成しましょう。

```react:PublicRoute.tsx
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にリダイレクトされ、認証済の場合のみ、プライベートルートにアクセスできます。

```react:PrivateRoute.tsx
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コンポーネントに統合しましょう。

```react:Router.tsx
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の一部分です。

- [GitHub - myrails-react-todo-v4](https://github.com/m17n-dev/myrails-react-todo-v4/tree/main/frontend/src/router)

## 参考文献

この記事は以下の情報を参考にして執筆しました。

- [How to Create Public And Private Routes using React Router](https://dev.to/nilanth/how-to-create-public-and-private-routes-using-react-router-72m)