はじめてのReact #25 「ルーティングに対応する」認証とリダイレクト編 react-router@v5

前々回では基本的なReactRouterの使い方を、前回でURLの一部をパラメーターとして受け取ってみました。

今回はユーザーの状態に合わせてページの出し分けを行います。 具体的には

  • 認証済みのユーザーにだけページを表示
  • 未認証のユーザーにはログインページを表示

という仕様になります。

名産品図鑑アプリを作る その3

実行結果

謎の地域「?????」をクリックすると認証ページへリダイレクトさせられます。ここでログインボタンをクリックすると、ログイン状態になるので再度「?????」をクリックすると内容が表示されます。 s3-us-west-2.amazonaws.com

ソースコード

App.js

import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Link, Switch, Redirect } from "react-router-dom";
import './App.css';

class App extends Component {
  render() {
    return (
      <Router>
        <div className={"container"}>
          {/* ナビゲーション */}
          <ul className={"gnavi"}>
            <li><Link to="/">名産品図鑑</Link></li>
            <li>┣ <Link to="/area/1">埼玉県</Link></li>
            <li>┣ <Link to="/area/2">島根県</Link></li>
            <li>┗ <Link to="/private">?????</Link></li>
            <li> </li>
            <li><Link to="/auth">認証</Link></li>
          </ul>

          {/* ここから下が実際のコンテンツに置き換わる */}
          <Switch>
            <Route exact path="/" component={Home} />
            <Route exact path="/index.html" component={Home} />
            <Route exact path="/auth" component={Auth} />
            <PrivateRoute path="/private" component={PrivatePage} auth="/auth" />
            <Route path="/area/:cd" component={Meisan} />
            <Route component={NoMatch}/>
          </Switch>
        </div>
      </Router>
    );
  }
}

/**
 * トップページ
 */
function Home(){
  return (
    <div className={"item"}>
      <h2>名産品図鑑</h2>
      <ul>
        <li><Link to="/area/1">埼玉県<br /><img src="/image/saitama.png" /></Link></li>
        <li><Link to="/area/2">島根県<br /><img src="/image/shimane.png" /></Link></li>
      </ul>
    </div>
  );
}

/**
 * 認証状態を保持する変数
 */
var UserStatus = {
  auth: false      //true:ログイン, false:未ログイン
};

/**
 * 簡易的な認証ページ
 */
class Auth extends Component{
  constructor(props){
    super(props);
    this.state = {
      auth: UserStatus.auth
    }
    this.onLogin  = this.onLogin.bind(this);
    this.onLogout = this.onLogout.bind(this);
  }
  onLogin(e){
    e.preventDefault();
    this.setState({auth:true});
    UserStatus.auth = true;
  }
  onLogout(e){
    e.preventDefault();
    this.setState({auth:false});
    UserStatus.auth = false;
  }
  render(){
    let desc   = UserStatus.auth?  "現在ログイン中です":"ログインしていません";
    let button = UserStatus.auth?  <button onClick={this.onLogout}>ログアウト</button>:<button onClick={this.onLogin}>ログイン</button>;
    return (
      <div className={"item"}>
        <h2>認証ページ</h2>
        <p>{desc}</p>
        <form>
          {button}
        </form>
      </div>
    );
  }
}

/**
 * ログインチェック付きRouter
 */
class PrivateRoute extends Component{
  constructor(props){
    super(props);
  }
  render(){
    if( UserStatus.auth ){
      return( <Route path={this.props.path} component={this.props.component} /> );
    }
    else{
      return( <Redirect to={this.props.auth} /> );
    }
  }
}

/**
 * 秘密のページ
 */
function PrivatePage(){
  return(
    <div className={"item"}>
      <h2>南極</h2>
      <img src="/image/nankyoku.png" />
      <ul>
        <li>ぺんぎん</li>
        <li>アザラシ</li>
        <li>オーロラ</li>
      </ul>
    </div>
  );
}


/**
 * 404
 */
function NoMatch(){
  return (
    <div className={"item"}>
      <h2>URLが存在しません</h2>
    </div>
  );
}

/**
 * 名産品 Component
 */
class Meisan extends Component{
  constructor(props){
    super(props);
    this.area =[
      {cd:1, name:"埼玉県", img:"/image/saitama.png", meisan:["十万石まんじゅう", "深谷ねぎ", "草加せんべい", "いも"]},
      {cd:2, name:"島根県", img:"/image/shimane.png", meisan:["しじみ", "あご野焼", "出雲そば", "ぶどう"]}
    ];
  }

  render(){
    let cd = this.props.match.params.cd;

    if( (0 < cd) && (cd <= this.area.length) ){
      let area = this.area[cd - 1];
      let key  = 1;
      let li = area.meisan.map( val => <li key={key++}>{val}</li> );
      return (
        <div className={"item"}>
          <h2>{area.name}</h2>
          <img src={area.img} />
          <ul>
            {li}
          </ul>
        </div>
      );
    }
    else{
      return (
        <div className={"item"}>
          <h2>Not Found</h2>
        </div>
      );
    }
  }
}

export default App;

App.css

CSSは前回から変更はありません。

a:hover{
  color: red;
}
ul{
  list-style: none;
}

.container{
  display: inline-flex;  /* 横に並べる */
}
.gnavi{
  order: 1;
  width: 80px;
  height: 500px;
  padding: 10px;
  margin-right: 10px;
  background-color: skyblue;
}
.item{
  order: 2;
}

解説

認証チェックは独自実装が必要

ReactRouterには認証状態をチェックしてルーティングする機能が存在しないため、独自に実装する必要があるようです。 今回はComponentを新規に作成しました。

<Switch>
  <PrivateRoute path="/private" component={PrivatePage} auth="/auth" />
</Switch>

このComponentが行っていることは非常にシンプルです。 認証状態を保持しているグローバル変数がtrueになっていればを、falseならを返しているだけです。

class PrivateRoute extends Component{
  constructor(props){
    super(props);
  }
  render(){
    if( UserStatus.auth ){
      return( <Route path={this.props.path} component={this.props.component} /> );
    }
    else{
      return( <Redirect to={this.props.auth} /> );
    }
  }
}

ログイン状態の切り替えは、こちらも簡易的に作成したComponentで行っています。ボタンを押したら認証状態を保持しているグローバル変数のtruefalseが入れ替わるだけのシンプルな物です。実際のアプリではここでサーバと通信して認証結果を取ってくることになります。

クラスでComponentを作成している関係で若干コードが長くなっていますが、原理としては非常に簡単ですね。

続き

blog.katsubemakito.net

参考ページ