はじめてのReact #26 「ルーティングに対応する」設定をJSONにまとめる編 react-router@v5

これまでルーティング関連の記事では以下のような内容を取り上げてきました。

  • 第23回 基本的なReactRouterの使い方
  • 第24回 URLの一部をパラメーターとして受け取る
  • 第25回 認証とリダイレクト

今までは以下のように直接を書いてルーティングの定義を行ってきましたが、今回はこれらの設定をJSONなどで定義し、いつでも外部のファイルに出せるようにします。

<!-- befor -->
<Switch>
  <Route exact path="/" component={Home} />
  <Route exact path="/auth" component={Auth} />
  <Route path="/area/:cd" component={Meisan} />
</Switch>

このままでも動くんですけどね。ロジックと設定は分離して置いた方がメンテしやすくなりますからね。

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

実行結果

挙動はこれまでと同じです。 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 {
  constructor(props){
    super(props);
    // ここにルーティングの設定をまとめる
    this.routes = [
      {path:"/",           component:Home,        type:"public", exact:true},
      {path:"/index.html", component:Home,        type:"public", exact:true},
      {path:"/auth",       component:Auth,        type:"public"},
      {path:"/private",    component:PrivatePage, type:"private", auth:"/auth"},
      {path:"/area/:cd",   component:Meisan,      type:"public"},
      {component:NoMatch,  type:"public"}
    ];
  }

  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="/404">404</Link></li>
            <li>┗ <Link to="/private">?????</Link></li>
            <li> </li>
            <li><Link to="/auth">認証</Link></li>
          </ul>

          {/* ここから下が実際のコンテンツに置き換わる */}
          <Switch>
            {this.routes.map((route, i) => {
              let type = route.type;
              delete route.type;
              switch( type ){
                case "public":
                  return( <Route key={i} {...route} /> );
                case "private":
                  return( <PrivateRoute key={i} {...route} /> );
              }
            })}
          </Switch>
        </div>
      </Router>
    );
  }
}

/*====================================================*/
/*           ここから下は前回と同じです               */
/*====================================================*/

/**
 * トップページ
 */
function Home(){
  return (
    <div className={"item"}>
      <h2>名産品図鑑</h2>
      <ul>
        <li><Link to="/area/1">埼玉県<br /><img src="/image/saitama.png" alt="埼玉県" /></Link></li>
        <li><Link to="/area/2">島根県<br /><img src="/image/shimane.png" alt="島根県" /></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" alt="北極" />
      <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} alt="area.name" />
          <ul>
            {li}
          </ul>
        </div>
      );
    }
    else{
      return (
        <div className={"item"}>
          <h2>Not Found</h2>
        </div>
      );
    }
  }
}

export default App;

App.css

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

ul{
  list-style: none;
}

a:hover{
  color: red;
}

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

解説

設定をJSON化

今回はAppクラス内で定義していますが、実際には外部のファイルに出しimportしてあげるのが良いでしょうか。

  constructor(props){
    super(props);
    // ここにルーティングの設定をまとめる
    this.routes = [
      {path:"/",           component:Home,        type:"public", exact:true},
      {path:"/index.html", component:Home,        type:"public", exact:true},
      {path:"/auth",       component:Auth,        type:"public"},
      {path:"/private",    component:PrivatePage, type:"private", auth:"/auth"},
      {path:"/area/:cd",   component:Meisan,      type:"public"},
      {component:NoMatch,  type:"public"}
    ];
  }
  • 各項目はに渡す値をそのまま書いてあげます。
  • type属性はComponentに切り替えに利用する物です。最終的に削除します。
  • exact属性のように属性名だけ渡す項目は、trueを値として指定しておきます。

JSONをに変換

this.routes配列をぐるぐる回しながら、type毎にComponentを出力しています。

<Switch>
  {this.routes.map((route, i) => {
    let type = route.type;
    delete route.type;

    switch( type ){
      case "public":
        return( <Route key={i} {...route} /> );
      case "private":
        return( <PrivateRoute key={i} {...route} /> );
    }
  })}
</Switch>

ドットが連続しているコイツ→...ですが、これはスプレッド演算子と呼ばれている、ES2015から追加された新機能です。何か省略しているわけではありませんw

例えば以下のような場合、funcには配列そのものが渡されますが、

let foo = [1, 2, 3];
func1(foo);

function func1(a){ console.log(a); }  

スプレッド演算子を利用した場合は、配列が展開された状態で渡されます。

let foo = [1, 2, 3];
func2(...foo);

function func2(a, b, c){ console.log(a, b, c); }

まぁこれだけだとピンと来ないと思いますので、上記のコードをブラウザのConsoleなどで実際に実行してみてください。

参考ページ