JavaScript
reactjs
react-router

今から始めるReact入門 〜 React Router


目次


Single Page Application (SPA)について

今回はreact-router を使用して宣言的記法を利用したSingle Page Application (SPA) の作成について勉強していきたいと思います。

React Router はUI とURL を同期させるライブラリで、例えば閲覧者がReact アプリケーションにアクセスした時にhttp://app.example.com/ とアクセスした時はHome のコンポーネントをレンダリングし、http://app.example.com/address とアクセスした時は住所を表示するコンポーネントをレンダリングしたりといった操作を簡単にできるようになります。


本記事で使用するファイル

本記事で使用するファイルは以下のリポジトリにあります。


プロジェクトの作成

前回のプロジェクトとは別に完全に新しいプロジェクトを作成します。

今回のアプリはBootstrap のテーマを利用していきますが、Bootstrap は一般的にモーションを作成するのにjQuery を必要としていますが、ちょっとしたサンプル程度で今回はjQuery によるモーションは不要なためjQuery はロードしません。


command

$ mkdir react_router

$ cd react_router
$ npm init -y
$ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader \
webpack webpack-cli webpack-dev-server \
react react-dom \
react-router react-router-dom

package.json のscripts にstart コマンドでwebpack-dev-server が起動するように設定します。


package.json

  "scripts": {

"start": "webpack-dev-server --content-base src --mode development --inline", // <- 追加
......
},

今回は、事前に静的なWeb ページとして、ある程度完成しているものをReact (JavaScript) 側へ移行して動的にレンダリングするように修正していきたいと思います。

これより以下のファイルはGitHub リポジトリを参考に準備してください。以下の状態からアプリケーション開発を進めていきます。


webpack.config.js

var debug   = process.env.NODE_ENV !== "production";

var webpack = require('webpack');
var path = require('path');

module.exports = {
context: path.join(__dirname, "src"),
entry: "./js/client.js",
module: {
rules: [{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
use: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-env']
}
}]
}]
},
output: {
path: __dirname + "/src/",
filename: "client.min.js",
publicPath: '/'
},
devServer: {
historyApiFallback: true
},
plugins: debug ? [] : [
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
],
};



src/js/pages/Layout.js

import React from "react";

export default class Layout extends React.Component {
render() {
return (
<h1>KillerNews.net</h1>
);
}
}



src/index.html

<!DOCTYPE html>

<html lang="en">

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>React</title>
<!-- Bootstrap Core CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cerulean/bootstrap.min.css" rel="stylesheet">

<!-- Custom Fonts -->
<!-- <link href="font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css"> -->
<link href="http://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700,300italic,400italic,700italic" rel="stylesheet" type="text/css">
</head>

<body>

<!-- Navigation -->
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li>
<a href="#">Featured</a>
</li>
<li>
<a href="#">Archives</a>
</li>
<li>
<a href="#">Settings</a>
</li>
</ul>
</div>
<!-- /.navbar-collapse -->
</div>
</nav>
<!-- Page Content -->
<div class="container" style="margin-top: 60px;">
<div class="row">
<div class="col-lg-12">
<div id="app"></div>
</div>
</div>
<!-- Call to Action Well -->
<div class="row">
<div class="col-lg-12">
<div class="well text-center">
Ad spot goes here
</div>
</div>
<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
<!-- Content Row -->
<div class="row">
<div class="col-md-4">
<h2>Heading 1</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe rem nisi accusamus error velit animi non ipsa placeat. Recusandae, suscipit, soluta quibusdam accusamus a veniam quaerat eveniet eligendi dolor consectetur.</p>
<a class="btn btn-default" href="#">More Info</a>
</div>
<!-- /.col-md-4 -->
<div class="col-md-4">
<h2>Heading 2</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe rem nisi accusamus error velit animi non ipsa placeat. Recusandae, suscipit, soluta quibusdam accusamus a veniam quaerat eveniet eligendi dolor consectetur.</p>
<a class="btn btn-default" href="#">More Info</a>
</div>
<!-- /.col-md-4 -->
<div class="col-md-4">
<h2>Heading 3</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe rem nisi accusamus error velit animi non ipsa placeat. Recusandae, suscipit, soluta quibusdam accusamus a veniam quaerat eveniet eligendi dolor consectetur.</p>
<a class="btn btn-default" href="#">More Info</a>
</div>
<!-- /.col-md-4 -->
</div>
<!-- /.row -->
<!-- Footer -->
<footer>
<div class="row">
<div class="col-lg-12">
<p>Copyright &copy; KillerNews.net</p>
</div>
</div>
</footer>
</div>

<!-- /.container -->
<script src="client.min.js"></script>
</body>
</html>



src/js/client.js

import React from "react";

import ReactDOM from "react-dom";

import Layout from "./pages/Layout";

const app = document.getElementById('app');

ReactDOM.render(<Layout />, app);



src/js/pages/Layout.js

import React from "react";

export default class Layout extends React.Component {
render() {
return (
<h1>KillerNews.net</h1>
);
}
}


ここまで準備できたら、一旦Web サーバを起動してページを確認してみましょう。


console

$ npm start


サーバが起動後、http://localhost:8080 へWeb ブラウザでアクセスします。

React_ReactRouter0001.png

このページをReact router を使ってSPA として動的に画面描写を変えていくようにしたいと思います。


React Router の導入

React の環境が完成したら次はreact-router 関連のパッケージをインストールします。


console

$ npm install --save-dev react-router react-router-dom

$ # react-router v4 からはreact-router-dom も必要になります

必要なライブラリをインストールしたら、各コンポーネントを作成していきます。

まずは記事をレンダリングするFeatured コンポーネントを作成します。


src/js/pages/Featured.js

import React from "react";

export default class Featured extends React.Component {
render() {
return (
<h1>Featured</h1>
);
}
}



src/js/pages/Archives.js

import React from "react";

export default class Archives extends React.Component {
render() {
return (
<h1>Archives</h1>
);
}
}



src/js/pages/Settings.js(new)

import React from "react";

export default class Settings extends React.Component {
render() {
return (
<h1>Settings</h1>
);
}
}


各コンポーネントの下地は作成完了しました。次はclient.js ファイルを編集して、React Router を使っていきます。


src/js/client.js

 import React from "react";

import ReactDOM from "react-dom";
+import { BrowserRouter as Router, Route } from "react-router-dom";

import Layout from "./pages/Layout";
+import Featured from "./pages/Featured";
+import Archives from "./pages/Archives";
+import Settings from "./pages/Settings";

const app = document.getElementById('app');

-ReactDOM.render(<Layout />, app);
+ReactDOM.render(
+ <Router>
+ <Layout>
+ <Route exact path="/" component={Featured}></Route>
+ <Route path="/archives" component={Archives}></Route>
+ <Route path="/settings" component={Settings}></Route>
+ </Layout>
+ </Router>,
+app);


上記のようにコンポーネントを列挙するとReact Router の特徴がわかりやすいと思いますが、React Router ではユーザがそれぞれ/, /archives, /settings のパスにアクセスした時に表示されるコンポーネントをそれぞれ、Featured, Archives, Settings になるように上記のように指定できます。

また、上記のReact Router の<Route exact path="/" component={Featured}></Route> のみexact というキーワードがついている点にも注目してください。

これはユーザがアクセスしたパスが/ と厳密にマッチしたときのみ表示されるコンポーネントであることを意味します。

http://app.example.com/ というURL にアクセスした時に表示されるコンポーネントになります。

このexact というキーワードが無いと/foo にも/bar にも/archives にもFeatured コンポーネントが表示されることになるので注意してください。

要するにユーザが/archives というパスにアクセスするとFeatured コンポーネントもArchives コンポーネントも表示されるという事態が発生します。

またこのexactを使ったコンポーネントの指定はreact-router v3 までのIndexRoute に相当しますので、react-router v4 からはexact を使うようにしてください。

また他のexactがない<Route path="/archives" component={Archives}></Route> はユーザが/archives/foo, /archives/bar とアクセスした場合にも表示されるコンポーネントとなります。

…という感じでReact Router のおおまかな特徴がわかったところで、もう一度npm start してhttp://localhost:8080 を確認してみましょう。

一番最初の時と同じレイアウトで、エラー無く表示されればOK です。とりあえずここまでできればReact Router の次のステップへの準備は完了です。


Link の追加

Layout.js を修正しArchive, Settingsへのリンクを追加してみましょう。


src/js/pages/Layout.js

 import React from "react";

import { Link } from "react-router-dom";

export default class Layout extends React.Component {
render() {
return (
<div>
<h1>KillerNews.net</h1>
+ {this.props.children}
+ <Link to="archives">archives</Link>,
+ <Link to="settings">settings</Link>
</div>
);
}
}


上記の記述をすることで、各リンクをクリックするとclient.js に埋め込まれたRoute コンポーネントがLayout コンポーネントへ渡されるようになり、Layout コンポーネントのthis.props.children からRoute コンポーネントを参照することができるようになります。


src/js/client.js


ReactDOM.render(
<Router>
<Layout> // (2) Layout に渡り、Layout からthis.props.children で参照できる
<Route exact path="/" component={Featured}></Route>
<Route path="/archives" component={Archives}></Route> // (1) 例えばLayout archives をクリックするとArchives ...
<Route path="/settings" component={Settings}></Route>
</Layout>
</Router>,
app);

ここで再度http://localhost:8080 を表示してarchives とsettings のリンクをそれぞれクリックしてみましょう。

各Archives コンポーネントとSettings コンポーネントが交互に切り替わって表示されるでしょう。

React_ReactRouter0002.gif


Link をbutton で装飾してみる


src/js/pages/Layout.js

 import React from "react";

import { Link } from "react-router-dom";

export default class Layout extends React.Component {
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
- <Link to="archives">archives</Link>
- <Link to="settings">settings</Link>
+ <Link to="archives"><button class="btn btn-danger">archives</button></Link>
+ <Link to="settings"><button class="btn btn-success">settings</button></Link>
</div>
);
}
}


button 要素に対してclass="btn btn-danger" とすることで、BootStrap のCSS が適用され、装飾されたボタンが表示されるようになります。

が、実のところclass というキーワードはJavaScript の予約語なので、JavaScript のソースコード内に、このように利用することができません。

実際、JSX ではclass では無く、className を使ってHTML のclass 属性を表すようになっています。

しかし、class キーワードがJavaScript の予約後だからといってJSX 内で使えないようでは、JavaScript の中にわざわざHTML タグライクなJSX を取り入れた利点が薄くなってしまうでしょう…。

そう思う場合はbabel-plugin-react-html-attrs を取り入れて、JSX 内でもclass キーワードを使えるようにしてしまいましょう。


babel-plugin-react-html-attrsインストール

$ npm install --save-dev babel-plugin-react-html-attrs



webpack.config.js

 var debug   = process.env.NODE_ENV !== "production";

var webpack = require('webpack');
var path = require('path');

module.exports = {
context: path.join(__dirname, "src"),
entry: "./js/client.js",
module: {
rules: [{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
use: [{
loader: 'babel-loader',
options: {
+ plugins: ['react-html-attrs'],
presets: ['@babel/preset-react', '@babel/preset-env']
}
}]
}]
},
output: {
path: __dirname + "/src/",
filename: "client.min.js"
},
plugins: debug ? [] : [
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
],
};


ここで、もう一度npm start コマンドを実行し直してください。

その後、Web ブラウザでhttp://localhost:8080 をもう一度確認してみてください。

React_ReactRouter0003.png

ボタンがBootstrap のCSS で装飾されていることが確認できましたでしょうか?

このようにJSX はHTML と同じようにclass(babel-plugin-react-html-attrs が無い場合はclassName) 属性を使用してHTML のclass 属性を指定することができるようになります。


補足: その他のclass 属性の指定方法

上記のソースコードではbutton 要素を作成して、そこにclass 属性を追加しましたが、react-router-dom のLink コンポーネントにclass 属性を追加しても同様なことができます。


src/js/pages/Layout.js

 import React from "react";

import { Link } from "react-router-dom";

export default class Layout extends React.Component {
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
- <Link to="archives">archives</Link>
- <Link to="settings">settings</Link>
+ <Link to="archives" class="btn btn-danger">archives</Link>
+ <Link to="settings" class="btn btn-success">settings</Link>
</div>
);
}
}


Web ブラウザでhttp://localhost:8080 を確認すると先ほどと変わらずに装飾されたボタンが表示されます。


navigate

React Router では普通に使っている状態で、各ボタンを押下してPath が変わった時に、表示されていた状態がブラウザの履歴にも蓄積されるようになります。

そのため、何かボタンを押下してからブラウザの戻るボタンを押下すると一つ前の状態に戻ることが確認できます。

今回構築しているアプリはシングルページアプリケーション(SPA) で画面遷移をしていないので、戻るボタンを押下した場合はアプリケーションを開く前のページ(http://localhost:8080 を表示する前に表示されていたサイト) に遷移することになるかと思われがちですが、そうはなりません。

navigate を使ってこの履歴の管理をもう少し柔軟にカスタマイズしてみましょう。

ただし、react-router v4 からnavigate を使って履歴を管理するにはwithRouter を使用することになります。

そのため、複雑になることを避けるために特に理由がない限りはデフォルトのままを利用したほうが良いでしょう。


  • this.props.history の関数一覧



    • this.props.history.push 画面遷移する。前居た画面を履歴に追加し、ブラウザの戻るボタンで戻れるようにする(React Router で標準的な挙動)。


    • this.props.history.replace 画面遷移する。前居た画面を置換するため、ブラウザの戻るボタンで戻れない。



例えば、this.props.history.push を使用する場合は次のようになります。


src/js/pages/Layout.js

 import React from "react";

-import { Link } from "react-router-dom";
+import { Link, withRouter } from "react-router-dom";

-export default class Layout extends React.Component {
+class Layout extends React.Component {
+ navigate() {
+ console.log(this.props.history);
+ this.props.history.push("/");
+ }
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
<Link to="archives" class="btn btn-danger">archives</Link>
<Link to="settings" class="btn btn-success">settings</Link>
+ <button class="btn btn-info" onClick={this.navigate.bind(this)}>featured</button>
</div>
);
}
}
-
+export default withRouter(Layout);

React_ReactRouter0004.gif

Web 画面を確認すると…

Archives クリック -> Featured クリック(push) -> Settings クリック -> 戻るボタン押下(Featured へ) -> 戻るボタン押下(Archives へ(pop)) -> 戻るボタン押下(初期画面へ) という流れになっており、history へのpush とpop が確認できました。

次はthis.props.history.replace() を使って履歴に残らない遷移を実装してみましょう。

:src/js/pages/Layout.js

import React from "react";
-import { Link } from "react-router-dom";
+import { Link, withRouter } from "react-router-dom";

-export default class Layout extends React.Component {
+class Layout extends React.Component {
navigate() {
console.log(this.props.history);
- this.props.history.push("/");
+ this.props.history.replace("/");
}
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
<Link to="archives" class="btn btn-danger">archives</Link>
<Link to="settings" class="btn btn-success">settings</Link>
<button class="btn btn-info" onClick={this.navigate.bind(this)}>featured</button>
</div>
);
}
}

+export default withRouter(Layout);

React_ReactRouter0004_02.gif

上記のようにすると、featured ボタンを押下した時に表示されていた画面(settings) は履歴に蓄積されていないことがわかると思います。featured 画面の履歴がなくなるというわけではない点に注意してください。

このようにボタンをクリックした時に、ブラウザの履歴に残したくない場合はthis.props.history.replace("/"); を使用するようにしましょう。

なお、ここで実施したステータスの変更は、次のレッスンに進むために元に戻しておくようにしてください。


src/js/pages/Layout.js

 import React from "react";

-import { Link } from "react-router-dom";
+import { Link, withRouter } from "react-router-dom";

-export default class Layout extends React.Component {
+class Layout extends React.Component {
navigate() {
console.log(this.props.history);
- this.props.history.replace("/");
+ this.props.history.push("/");
}
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
<Link to="archives" class="btn btn-danger">archives</Link>
<Link to="settings" class="btn btn-success">settings</Link>
<button class="btn btn-info" onClick={this.navigate.bind(this)}>featured</button>
</div>
);
}
}

+export default withRouter(Layout);



URL のパラメータ取得

ここではURL に情報を渡す方法として手っ取り早い方式であるGet パラメータを使ってアクセスされた時にURL パラメータから情報を取得する方法について勉強していきます。

例えば、http://localhost:8080/archives/some-article とURL が入力された時に"some-article" の部分を取得したい場合、:変数名 という形式で取得することができます。

またpath="/archives"path="/archives/:article" が部分一致するため、/archives/foo ロケーションにアクセスした時に両方のコンポーネントがレンダリングされることになります。

そのため、短い方のpath="/archives"exact キーワードを追加して完全一致した場合のみレンダリングするように変更します。


src/js/pages/client.js

 import React from "react";

import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";

import Layout from "./pages/Layout";
import Featured from "./pages/Featured";
import Archives from "./pages/Archives";
import Settings from "./pages/Settings";

const app = document.getElementById('app');

ReactDOM.render(
<Router>
<Layout>
<Route exact path="/" component={Featured}></Route>
- <Route path="/archives" component={Archives}></Route>
+ <Route exact path="/archives" component={Archives}></Route>
+ <Route path="/archives/:article" component={Archives}></Route>
<Route path="/settings" component={Settings}></Route>
</Layout>
</Router>,
app);


このGet パラメータの値は<Route path="/archives/:article" component={Archives}></Route> とすることでArchives コンポーネントへ/archives以下の値が渡るようになります。

上記の例では、具体的にArchives コンポーネント内でthis.props.match.params.article とすることでパラメータを取得することができます(もしどんなパラメータ名でArchives コンポーネントにパラメータが渡っているかわかりにくい場合は、Archives コンポーネント内でconsole.log(this.props); とすることで、渡っているパラメータ一覧が見れるので確認してみると良いでしょう)。


src/js/pages/Archives.js

 import React from "react";

export default class Archives extends React.Component {
render() {
return (
- <h1>Archives</h1>
+ <h1>Archives ({this.props.match.params.article})</h1>
);
}
}


Layout.js にリンクを追加


src/js/pages/Layout.js

 import React from "react";

import { Link, withRouter } from "react-router-dom";

class Layout extends React.Component {
navigate() {
console.log(this.props.history);
this.props.history.push("/");
}
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
+ <Link to="/archives/some-other-articles" class="btn btn-warning">archives (some other articles)</Link>
<Link to="/archives" class="btn btn-danger">archives</Link>
<Link to="/settings" class="btn btn-success">settings</Link>
<button class="btn btn-info" onClick={this.navigate.bind(this)}>feathred</button>
</div>
);
}
}
export default withRouter(Layout);


http://localhost:8080 をWeb ブラウザで表示して、archives (some other articles)archives リンクをクリックしてみましょう。

前者をクリックするとhttp://localhost:8080/archives/some-other-articles へ飛び、パラメータである(some-other-articles) が画面に表示サれるようになり、後者をクリックするとhttp://localhost:8080/archives へ飛び、空パラメータである() が表示されるようになっています。

React_ReactRouter0005.gif


URL のパラメータを正規表現で指定する

子コンポーネントに渡すパラメータ名ですが、正規表現で指定することができます。

例えば下記の例はSettings コンポーネントにthis.props.match.params.mode という変数名で"/settings/main" もしくは"/settings/extra" というパス値のときのみ動作するコンポーネントを定義することができます。


src/js/pages/client.js

 import React from "react";

import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";

import Layout from "./pages/Layout";
import Featured from "./pages/Featured";
import Archives from "./pages/Archives";
import Settings from "./pages/Settings";

const app = document.getElementById('app');

ReactDOM.render(
<Router>
<Layout>
<Route exact path="/" component={Featured}></Route>
<Route path="/archives/:article" component={Archives}></Route>
- <Route path="/settings" component={Settings}></Route>
+ <Route path="/settings/:mode(main|extra)" component={Settings}></Route>
</Layout>
</Router>,
app);


上記のように定義すると、Settings コンポーネントでthis.props.match.params.mode で値を参照できるようになります。


src/js/pages/Settings.js

 import React from "react";

export default class Settings extends React.Component {
render() {
+ const type = (this.props.match.params.mode == "extra"? " (for experts)": "");
return (
- <h1>Settings</h1>
+ <h1>Settings{type}</h1>
);
}
}


/settings/main, /settings/extra へのボタンを追加します。


src/js/pages/Layout.js

 import React from "react";

import { Link, withRouter } from "react-router-dom";

class Layout extends React.Component {
navigate() {
console.log(this.props.history);
this.props.history.push("/");
}
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
<Link to="/archives/some-other-articles" class="btn btn-warning">archives (some other articles)</Link>
<Link to="/archives" class="btn btn-danger">archives</Link>
- <Link to="/settings" class="btn btn-success">settings</Link>
+ <Link to="/settings/main" class="btn btn-success">settings</Link>
+ <Link to="/settings/extra" class="btn btn-success">settings (extra)</Link>
<button class="btn btn-info" onClick={this.navigate.bind(this)}>feathred</button>
</div>
);
}
}
export default withRouter(Layout);


ページを開いてsettings, settings (extra) ボタンをクリックしてみましょう。

React_ReactRouter0007.gif

上記のようにthis.props.match.param.mode の値、main, extra を受け取れていることがわかります。

またここではclient.js にて<Route path="/settings/:mode(main|extra)" component={Settings}></Route> という書き方をしていますが、もしmain でもなくextra でもない値が渡された場合はSettings コンポーネントのレンダリングは行われません。


クエリストリングを取得する

React Router でクエリストリングを扱う方法について紹介します。

クエリストリングとはブラウザなどでURL をhttp://localhost:8080/archives?date=today&filter=hot と入力した時に、パラメータとして渡す?date=today&filter=hot のkey=value の値のことになります。

しかし、このquery stringをReact Router で解析するやり方ですが、React Router v4 で撤去されてしまったようです。

なのでReact Router v4 からは独自にクエリストリングを解析する必要がありますが、ここではURLSearchParams を使った方法を紹介します。

まずはトップ画面にクエリストリングを含んだリンクボタンを追加します。


src/js/pages/Layout.js

 import React from "react";

import { Link, withRouter } from "react-router-dom";

class Layout extends React.Component {
navigate() {
console.log(this.props.history);
this.props.history.push("/");
}
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
- <Link to="/archives/some-other-articles" class="btn btn-warning">archives (some other articles)</Link>
- <Link to="/archives" class="btn btn-danger">archives</Link>
+ <Link to="/archives/some-other-articles?date=yesterday&filter=none" class="btn btn-warning">archives (some other articles)</Link>
+ <Link to="/archives?date=today&filter=hot" class="btn btn-danger">archives</Link>
<Link to="/settings/main" class="btn btn-success">settings</Link>
<Link to="/settings/extra" class="btn btn-success">settings (extra)</Link>
<button class="btn btn-info" onClick={this.navigate.bind(this)}>feathred</button>
</div>
);
}
}
export default withRouter(Layout);


次にArchives コンポーネントにクエリストリングを解析して、その結果を画面に表示するようにします。


src/js/pages/Archives.js

 import React from "react";

export default class Archives extends React.Component {
render() {
+ const query = new URLSearchParams(this.props.location.search)
+ let message
+ = (this.props.match.params.article ? this.props.match.params.article + ", ": "")
+ + "date=" + query.get("date") + ", filter=" + query.get("filter");
return (
- <h1>Archives ({this.props.match.params.article})</h1>
+ <h1>Archives ({message})</h1>
);
}
}


React_ReactRouter0009.gif

React Router v4 になってもURLSearchParams を使えばキレイにクエリパラメータを取得することができました。


補足: URLSearchParams をサポートしていないブラウザの場合

URLSearchParams をサポートしていないブラウザの場合、query-string ライブラリを使って解析する方法もありです。

詳細な説明は割愛します。


ボタンがアクティブの時のclass 指定(activeClassName)

NavLink を使うとactiveClassName というprop を使って該当するリンクが表示されている時に適用するHTML のclass 値を指定することができます。


src/js/pages/Layout.js

 import React from "react";

-import { Link, withRouter } from "react-router-dom";
+import { NavLink, Link, withRouter } from "react-router-dom";

class Layout extends React.Component {
navigate() {
console.log(this.props.history);
this.props.history.push("/");
}
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
<Link to="/archives/some-other-articles?date=yesterday&filter=none" class="btn btn-warning">archives (some other articles)</Link>
<Link to="/archives?date=today&filter=hot" class="btn btn-danger">archives</Link>
- <Link to="/settings/main" class="btn btn-success">settings</Link>
+ <NavLink to="/settings/main" class="btn btn-success" activeClassName="btn-danger">settings</NavLink>
<Link to="/settings/extra" class="btn btn-success">settings (extra)</Link>
<button class="btn btn-info" onClick={this.navigate.bind(this)}>feathred</button>
</div>
);
}
}
export default withRouter(Layout);

React_ReactRouter0011.gif

以上、React Router の基本的な機能についてはおおかた、解説はしてきました。

次は静的なHTML で記載されているページをReact のJSX に埋め込んでいき、アプリケーションを完成させていきます。

ここまでのソースコードは以下のリポジトリに格納しておきましたので、参考になればと思います。


補足: query string が含まれる場合のactiveClassName の挙動について

NavLink のto にquery string が含まれる場合、activeClassName に指定したclass が正しく反映されません。

これはバグではなくReact Router v4 からquery string に対しての機能を取り除いたためです。


html 静的コンテンツのコンポーネント化

src/index.html 内の静的コンテンツを各React コンポーネントに移動していきます。

今回は手順が膨大なので作成済みのコンテンツを元に、注目ポイントのみ見ていくようにします。

静的コンテンツ移行後のsrc/index.html の内容は以下のようにスッキリしたものになります。

以下の<div id="app"></div> 内にコンポーネント化したコンテンツでデータを描写していきます。


src/index.html

<!DOCTYPE html>

<html lang="en">

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>React</title>
<!-- Bootstrap Core CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cerulean/bootstrap.min.css" rel="stylesheet">

<!-- Custom Fonts -->
<!-- <link href="font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css"> -->
<link href="http://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700,300italic,400italic,700italic" rel="stylesheet" type="text/css">
</head>

<body>
<div id="app"></div>
<script src="/client.min.js"></script>
</body>

</html>


記事のテンプレートはsrc/js/components/Article.js で、それをsrc/js/pages/Featured.js からタイトルを設定して各記事を表示させるようにします。


src/js/components/Article.js

import React from "react";

export default class Article extends React.Component {
render() {
const { title } = this.props;

return (
<div class="col-md-4">
<h4>{title}</h4>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe rem nisi accusamus error velit animi non ipsa placeat. Recusandae, suscipit, soluta quibusdam accusamus a veniam quaerat eveniet eligendi dolor consectetur.</p>
<a class="btn btn-default" href="#">More Info</a>
</div>
);
}
}



src/js/pages/Featured.js

import React from "react";

import Article from "../components/Article";

export default class Featured extends React.Component {
render() {
const Articles = [
"Some Article",
"Some Other Article",
"Yet Another Article",
"Still More",
"Some Article",
"Some Other Article",
"Yet Another Article",
"Still More",
"Some Article",
"Some Other Article",
"Yet Another Article",
"Still More"
].map((title, i) => <Article key={i} title={title} />);

const adText = [
"Ad spot #1",
"Ad spot #2",
"Ad spot #3",
"Ad spot #4",
"Ad spot #5"
];

const randomAd = adText[Math.round(Math.random() * (adText.length - 1))];
console.log("featured");

return (
<div>
<div class="row">
<div class="col-lg-12">
<div class="well text-center">
{randomAd}
</div>
</div>
</div>
<div class="row">{Articles}</div>
</div>
);
}
}


また画面上部のメニューに関してはsrc/js/components/layout/Nav.js で現在表示されているlocation を取得して、該当するメニューボタンのみactive なスタイルを設定するように設定します。


src/js/components/layout/Nav.js

import React from "react";

import { Link } from "react-router-dom";

export default class Nav extends React.Component {
constructor() {
super();
this.state = {
collapsed: true
};
}
toggleCollapse() {
const collapsed = !this.state.collapsed;
this.setState({collapsed});
}
render() {
const { location } = this.props;
const { collapsed } = this.state;
const featuredClass = location.pathname === "/" ? "active" : "";
const archivesClass = location.pathname.match(/^\/archives/) ? "active" : "";
const settingsClass = location.pathname.match(/^\/settings/) ? "active" : "";
const navClass = collapsed ? "collapse" : "";
return (
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" onClick={this.toggleCollapse.bind(this)}>
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class={"navbar-collapse " + navClass} id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class={featuredClass}>
<Link to="/" onClick={this.toggleCollapse.bind(this)}>Featured</Link>
</li>
<li class={archivesClass}>
<Link to="/archives" onClick={this.toggleCollapse.bind(this)}>Archives</Link>
</li>
<li class={settingsClass}>
<Link to="/settings" onClick={this.toggleCollapse.bind(this)}>Settings</Link>
</li>
</ul>
</div>
</div>
</nav>
);
}
}


React_ReactRouter0012.gif


React Router v3 からの主な変更点一覧

最後に、今までReact Router v3 を利用していて最近のReact Router v4 を利用し始めたときに出くわした様々な変更点は下記URL の記事にまとめておきました。React Router v3 を今まで使っていた人の参考になればと思います。

React Router v4 からの主な移行・変更点一覧


参考