React.js の Context API で Private Route を作る

Reactでログイン機能があるSPAを開発する際に、特定のページ（ルート）を秘匿したい場合があると思います。
今回はReact v16から実装されたContext APIを使ってプライベートなルートを作成します。

# Context APIとは

Context APIはProviderとConsumerという機構を使い、異なるコンポーネント間でデータの共有を可能にするものです。大まかな概要は以下の通りです。

- createContextはProviderとConsumerというコンポーネントのペアを返す
- Providerはデータとコールバックを保有するコンポーネント
- ConsumerはProviderのデータを購読しコンポーネントにデータを渡す


# 環境

**ライブラリのバージョン**

Library | version
:---- | :----
react | 16.2.0
react-router-dom | 4.3.1


**ディレクトリ構成**

Reactではディレクトリ構成やコンポーネントの分割に規制がなく、アプリケーションの要件に応じて設計することができます。
以下は今回、解説に使うアプリケーションのディレクトリ構成です。あくまで例ですので、実際の開発では適宜設計してください。

```
index.js
  /contexts
    auth.js
  /routes
    home.js
    mypage.js
```

# 1. Contextを作成する

`/contexts`ディレクトリに`auth.js`ファイルを作成しAuthContextを作成します。

```jsx
// contexts/auth.js
import React, { Component } from 'react';
// Contextの生成
const AuthContext = React.createContext();
// ContextにはProviderとConsumerというコンポーネントが含まれる
const AuthProvider = AuthContext.Provider;
const AuthConsumer = AuthContext.Consumer;

export { AuthProvider, AuthConsumer };
```

AuthContextにログイン状態を表す`isAuth`というデータを管理させるため、Providerをカスタマイズします。

```jsx
// contexts/auth.js
...
class AuthProvider extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isAuth: false
    }
  }
  // Providerのvalueを設定する。Consumerはこのvalueを購読する。
  render() {
    return (
      <AuthContext.Provider
        value={{
          isAuth: this.state.isAuth
        }}
      >
        {this.props.children}
      </AuthContext.Provider>
    );
  }
}
...
```

# 2. Contextからデータを取り出す

AuthContextから`isAuth`を取り出しコンポーネントに反映させてみます。
Headerというコンポーネントを作成し、ログイン状態に応じてテキストを表示させます。

ProviderでHeaderコンポーネントをラップし、コンポーネント側ではConsumerを使ってデータを取り出します。

```jsx
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { AuthProvider, AuthConsumer } from './contexts/auth';
// ProviderでHeaderコンポーネントをラップ
const App = () => (
  <AuthProvider>
    <Header />
  </AuthProvider>
);
// Headerコンポーネント中でConsumerからデータを取り出す
const Header = () => (
  <AuthConsumer>
    {({ isAuth }) => (
      <>
        <h1>Header</h1>
        {isAuth ? (
          <p>ログインしているよ！</p>
        ) : (
          <p>ログインしていないよ！</p>
        )}
      </>
    )}
  </AuthConsumer>
);

ReactDOM.render(<App />, document.getElementById('root'));
```

# 3. Providerに状態を変更するコールバックを作成

ログイン、ログアウトボタンを作成するために、AuthProviderにログイン状態を切り替えるためのコールバック関数を作成します。
今回はfetch等のAPI通信は割愛します。

```jsx
// contexts/auth.js
...
class AuthProvider extends Component {
  constructor(props) {
    ...
    // thisをバインド
    this.login = this.login.bind(this);
    this.logout = this.logout.bind(this);
  }
  login() {
    // 実際にはここでAPI等を使ってログイン処理を行う
    setTimeout(() => this.setState({ isAuth: true }), 1000);
  }
  logout() {
    this.setState({ isAuth: false });
  }
  render() {
    return (
      <AuthContext.Provider
        value={{
          isAuth: this.state.isAuth,
          login: this.login,
          logout: this.logout
        }}
      >
        {this.props.children}
      </AuthContext.Provider>
    );
  }
}
...
```

続いてHeaderコンポーネントにログイン/ログアウトボタンを実装します。

```jsx
// index.js
...
const Header = () => (
  <AuthConsumer>
    {({ isAuth, login, logout }) => (
      <>
        <h1>Header</h1>
        {isAuth ? (
          <p>ログインしているよ！</p>
          <button onClick={logout}>ログアウト</button>
        ) : (
          <p>ログインしていないよ！</p>
          <button onClick={login}>ログイン</button>
        )}
      </>
    )}
  </AuthConsumer>
);
...
```

# 4. プライベートなルートを作成

次はreact-routerを使ってプライベートなルートを作成していきます。
Headerコンポーネントに続いて、react-routerを使ってルーティングを設定します。

```jsx
// index.js
...
import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom';
import Home from './routes/home';
import MyPage from './routes/mypage';
// Headerコンポーネントを取り除き、ルーティングを設定
const App = () => (
  <BrowserRouter>
    <AuthProvider>
      <Header />
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/mypage" component={MyPage} />
      </Switch>
    </AuthProvider>
  </BrowserRouter>
);
...
```

続いて、プライベートなルートを作成するための、PrivateRouteコンポーネントを作成します。

```jsx
// index.js
...
// プライベートなルートを設定するためのコンポーネント
const PrivateRoute = ({ component: Component, ...rest }) => (
  <AuthConsumer>
    {({ isAuth }) => (
      <Route
        render={props =>
          isAuth ? (
            <Component {...props} />
          ) : (
            <Redirect to="/" />
          )
        }
        {...rest}
      />
    )}
  </AuthConsumer>
);
// 秘匿したいルートをPrivateRouteで置き換える
const App = () => (
  <BrowserRouter>
    <AuthProvider>
      <Header />
      <Switch>
        <Route exact path="/" component={Home} />
        <PrivateRoute path="/mypage" component={MyPage} />
      </Switch>
    </AuthProvider>
  </BrowserRouter>
);
...
```

未ログイン状態でmypageにアクセスすると、トップページにリダイレクトされることが確認できると思います。

大規模なアプリケーションなど厳格な状態管理が必要とされるケースでは、Redux等の状態管理フレームワークが引き続き有用に思いますが、中小規模のアプリケーションにおいてはContextを用いるのも良い選択肢ではないでしょうか。


##### 参照サイト

- [Context - React](https://reactjs.org/docs/context.html)
- [How to protect your routes with React Context](https://medium.freecodecamp.org/how-to-protect-your-routes-with-react-context-717670c4713a)