Reactでログイン機能を実装する

# はじめに
react-router-domでPrivateRouteを実現します。今回は以下の機能を実装していきたいと思います。

1. ログイン済みユーザーしかアクセスできないページを作る
2. ↑のページに、ログインせずにアクセスしようとするとログインページにリダイレクトされる。
3. 2でログインページにリダイレクトされた後、ログインを行えば元のページ（アクセスしたかったページ）に遷移する。

画面構成は以下のようにします。

+ `/login`： ログイン画面。未ログインユーザーしかアクセスできない。
+ `/`： ホーム画面。ログイン済みユーザーしかアクセスできない。
+ `/profile/:userId`： ユーザーのプロフィール画面。ログイン済みユーザーしかアクセスできない。

プロジェクトは[Githubのレポジトリ](https://github.com/yuta-ike/private-router-sample)にあげているのでよければご覧ください。

## プロジェクトを作る
今回はcreate-react-appを用いてセットアップします。次に`react-router-dom`をインストールします

```zsh
npx create-react-app private-router-sample --template typescript
npm install react-router-dom @types/react-router-dom
```

## PrivateRouteを実装する
今回の肝となるPrivateRouteの実装です。（`useAuthUser`, `AuthUserProvider`については別ファイルで定義しています。詳しい実装はGithubをご覧ください）

```App.tsx
import React from 'react';
import { BrowserRouter as Router, Redirect, Route, RouteProps, Switch } from 'react-router-dom';

// PrivateRouteの実装
const PrivateRoute: React.FC<RouteProps> = ({...props}) => {
  const authUser = useAuthUser()
  const isAuthenticated = authUser != null //認証されているかの判定
  if (isAuthenticated) {
    return <Route {...props}/>
  }else{
    console.log(`ログインしていないユーザーは${props.path}へはアクセスできません`)
    return <Redirect to="/login"/>
  }
}

const App: React.FC = () => {
  return (
    <AuthUserProvider>
      <Router>
        <Switch>
          <Route exact path="/login" component={LoginPage}/>

         {/* Routeと同じ形で使用 */}
          <PrivateRoute exact path="/" component={HomePage} /> 
          <PrivateRoute exact path="/profile/:userId" component={ProfilePage}/>
        </Switch>
      </Router>
    </AuthUserProvider>
  );
}
```


ポイントは、`PrivateRoute`内の以下の実装です。

```tsx
if (isAuthenticated) {
  return <Route {...props}/>
} else {
  console.log(`ログインしていないユーザーは${props.path}へはアクセスできません`)
  return <Redirect to="/login"/>
}
```

認証済身なら`Route`を、認証されていなければ`Redirect`を返すことでログインページへのリダイレクトを行っています。


## UnAuthRoute

上記の実装では、「未ログインのユーザーが`/`にアクセス」することは防げましたが、「ログイン済みのユーザーが`/login`にアクセス」することは防げていません。そこで、ログイン済みユーザーが`/login`へアクセスした場合、`/`へリダイレクトする処理を実装します。

今回は`PrivateRoute`と逆の処理を行う`UnAuthRoute`を実装したいと思います。

```App.tsx
import React from 'react';
import { BrowserRouter as Router, Redirect, Route, RouteProps, Switch } from 'react-router-dom';

// PrivateRouteの実装
const PrivateRoute: React.FC<RouteProps> = ({...props}) => {
  const authUser = useAuthUser()
  const isAuthenticated = authUser != null //認証されているかの判定
  if (isAuthenticated) {
    return <Route {...props}/>
  }else{
    console.log(`ログインしていないユーザーは${props.path}へはアクセスできません`)
    return <Redirect to="/login"/>
  }
}

// ===追加部分 ==========
// UnAuthRouteの実装
const UnAuthRoute: React.FC<RouteProps> = ({ ...props }) => {
  const authUser = useAuthUser()
  const isAuthenticated = authUser != null
  if (isAuthenticated) {
    console.log(`ログイン済みのユーザーは${props.path}へはアクセスできません`)
    return <Redirect to="/" />
  } else {
    return <Route {...props} />
  }
}
// ===追加部分ここまで ======

const App: React.FC = () => {
  return (
    <AuthUserProvider>
      <Router>
        <Switch>
         {/* 未ログインユーザーのみアクセス可能 */}
          <UnAuthRoute exact path="/login" component={LoginPage}/>
         {/* ログイン済みユーザーのみアクセス可能 */}
          <PrivateRoute exact path="/" component={HomePage} /> 
          <PrivateRoute exact path="/profile/:userId" component={ProfilePage}/>
        </Switch>
      </Router>
    </AuthUserProvider>
  );
}

```

## 元のページへリダイレクト
ここまでの実装では、3の要件を満たしていません。

例えば、未ログインのユーザーが`/profile/1`にアクセスしたときの処理の流れは以下のようになります

```
1. /profile/1にアクセス
2. PrivateRouteによって/loginにリダイレクトされる
3. ログインが完了すればUnAuthRouteによって/にリダイレクトされる ← 要件を満たすならば`/profile/1`へリダイレクトされるべきです。
```



そこで、`PrivateRoute`、`UnAuthRoute`を以下のように修正します。


```App.tsx
const PrivateRoute: React.FC<RouteProps> = ({...props}) => {
  const authUser = useAuthUser()
  const isAuthenticated = authUser != null
  if (isAuthenticated) {
    return <Route {...props}/>
  }else{
    console.log(`ログインしていないユーザーは${props.path}へはアクセスできません`)
    return <Redirect to={{pathname: "/login", state: { from: props.location?.pathname }}}/> // fromに本来アクセス
  }
}

const UnAuthRoute: React.FC<RouteProps> = ({ ...props }) => {
  const authUser = useAuthUser()
  const isAuthenticated = authUser != null
  const { from } = useLocation<{from: string | undefined}>().state

  if (isAuthenticated) {
    console.log(`ログイン済みのユーザーは${props.path}へはアクセスできません`)
    return <Redirect to={from ?? "/"} />
  } else {
    return <Route {...props} />
  }
}
```

実装のポイントはPrivateRouteの `<Redirect to={{pathname: "/login", state: { from: props.location?.pathname }}}/>`です。PrivateRouteは未ログインユーザーをログインページにリダイレクトしますが、その際にユーザーがアクセスしようとしたURLをstateとして設定しています。

ここで設定したstateはUnAuthRouteで `const { from } = useLocation<{from: string | undefined}>().state`という形で取り出されます。取り出されたURLは、`return <Redirect to={from ?? "/"} />`でリダイレクト先として指定されます。（`?? "/"`が指定されているのは、ユーザーが直接ログインページにアクセスした場合は`from == null`となるからです）


# 最後に
今回はログイン/未ログインの2状態を扱いましたが、サービスによってはユーザー登録が絡む場合もあるでしょう。その場合には、未ログイン/未登録/登録済みの3状態を扱うことになりますが、`UnRegisteredRoute`を追加するなどで応用できます。