目次
- 今から始めるReact入門 〜 React の基本
- 今から始めるReact入門 〜 React Router 編 ←★ここ
- 今から始めるReact入門 〜 flux編
- 今から始めるReact入門 〜 Redux 編: immutability とは
- 今から始めるReact入門 〜 Redux 編: Redux 単体で状態管理をしっかり理解する
- 今から始めるReact入門 〜 Redux 編: Redux アプリケーションを作成する
- 今から始めるReact入門 〜 Mobx 編
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 はロードしません。
$ 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 が起動するように設定します。
"scripts": {
"start": "webpack-dev-server --content-base src --mode development --inline", // <- 追加
......
},
今回は、事前に静的なWeb ページとして、ある程度完成しているものをReact (JavaScript) 側へ移行して動的にレンダリングするように修正していきたいと思います。
これより以下のファイルはGitHub リポジトリを参考に準備してください。以下の状態からアプリケーション開発を進めていきます。
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 }),
],
};
import React from "react";
export default class Layout extends React.Component {
render() {
return (
<h1>KillerNews.net</h1>
);
}
}
<!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 © KillerNews.net</p>
</div>
</div>
</footer>
</div>
<!-- /.container -->
<script src="client.min.js"></script>
</body>
</html>
import React from "react";
import ReactDOM from "react-dom";
import Layout from "./pages/Layout";
const app = document.getElementById('app');
ReactDOM.render(<Layout />, app);
ここまで準備できたら、一旦Web サーバを起動してページを確認してみましょう。
$ npm start
サーバが起動後、http://localhost:8080
へWeb ブラウザでアクセスします。
このページをReact router を使ってSPA として動的に画面描写を変えていくようにしたいと思います。
React Router の導入
React の環境が完成したら次はreact-router 関連のパッケージをインストールします。
$ npm install --save-dev react-router react-router-dom
$ # react-router v4 からはreact-router-dom も必要になります
必要なライブラリをインストールしたら、各コンポーネントを作成していきます。
まずは記事をレンダリングするFeatured コンポーネントを作成します。
import React from "react";
export default class Featured extends React.Component {
render() {
return (
<h1>Featured</h1>
);
}
}
import React from "react";
export default class Archives extends React.Component {
render() {
return (
<h1>Archives</h1>
);
}
}
import React from "react";
export default class Settings extends React.Component {
render() {
return (
<h1>Settings</h1>
);
}
}
各コンポーネントの下地は作成完了しました。次はclient.js ファイルを編集して、React Router を使っていきます。
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へのリンクを追加してみましょう。
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 コンポーネントを参照することができるようになります。
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 コンポーネントが交互に切り替わって表示されるでしょう。
Link をbutton で装飾してみる
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
キーワードを使えるようにしてしまいましょう。
$ npm install --save-dev babel-plugin-react-html-attrs
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
をもう一度確認してみてください。
ボタンがBootstrap のCSS で装飾されていることが確認できましたでしょうか?
このようにJSX はHTML と同じようにclass
(babel-plugin-react-html-attrs が無い場合はclassName) 属性を使用してHTML のclass 属性を指定することができるようになります。
補足: その他のclass 属性の指定方法
上記のソースコードではbutton 要素を作成して、そこにclass 属性を追加しましたが、react-router-dom のLink コンポーネントにclass 属性を追加しても同様なことができます。
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
を使用する場合は次のようになります。
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);
Web 画面を確認すると…
Archives クリック -> Featured クリック(push) -> Settings クリック -> 戻るボタン押下(Featured へ) -> 戻るボタン押下(Archives へ(pop)) -> 戻るボタン押下(初期画面へ) という流れになっており、history へのpush とpop が確認できました。
次はthis.props.history.replace()
を使って履歴に残らない遷移を実装してみましょう。
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("/");
+ 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);
上記のようにすると、featured ボタンを押下した時に表示されていた画面(settings) は履歴に蓄積されていないことがわかると思います。featured 画面の履歴がなくなるというわけではない点に注意してください。
このようにボタンをクリックした時に、ブラウザの履歴に残したくない場合はthis.props.history.replace("/");
を使用するようにしましょう。
なお、ここで実施したステータスの変更は、次のレッスンに進むために元に戻しておくようにしてください。
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
キーワードを追加して完全一致した場合のみレンダリングするように変更します。
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);
とすることで、渡っているパラメータ一覧が見れるので確認してみると良いでしょう)。
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 にリンクを追加
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
へ飛び、空パラメータである()
が表示されるようになっています。
URL のパラメータを正規表現で指定する
子コンポーネントに渡すパラメータ名ですが、正規表現で指定することができます。
例えば下記の例はSettings コンポーネントにthis.props.match.params.mode
という変数名で"/settings/main" もしくは"/settings/extra" というパス値のときのみ動作するコンポーネントを定義することができます。
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
で値を参照できるようになります。
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
へのボタンを追加します。
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)
ボタンをクリックしてみましょう。
上記のように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 を使った方法を紹介します。
まずはトップ画面にクエリストリングを含んだリンクボタンを追加します。
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 コンポーネントにクエリストリングを解析して、その結果を画面に表示するようにします。
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 Router v4 になってもURLSearchParams を使えばキレイにクエリパラメータを取得することができました。
補足: URLSearchParams をサポートしていないブラウザの場合
URLSearchParams をサポートしていないブラウザの場合、query-string
ライブラリを使って解析する方法もありです。
詳細な説明は割愛します。
ボタンがアクティブの時のclass 指定(activeClassName)
NavLink を使うとactiveClassName というprop を使って該当するリンクが表示されている時に適用するHTML のclass 値を指定することができます。
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 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>
内にコンポーネント化したコンテンツでデータを描写していきます。
<!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
からタイトルを設定して各記事を表示させるようにします。
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>
);
}
}
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 なスタイルを設定するように設定します。
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 Router v3 からの主な変更点一覧
最後に、今までReact Router v3 を利用していて最近のReact Router v4 を利用し始めたときに出くわした様々な変更点は下記URL の記事にまとめておきました。React Router v3 を今まで使っていた人の参考になればと思います。
React Router v4 からの主な移行・変更点一覧
参考
-
REACT JS TUTORIAL #7 - React Router Params & Queries
-
Start Bootstrap
-
BootstrapCDN
-
react router v^4.0.0 Uncaught TypeError: Cannot read property 'location' of undefined
-
Using React IndexRoute in react-router v4
-
Nested Routes in React Router v4
-
Warning: Unknown DOM property class. Did you mean className?
-
The prop 'history' is marked as required in 'Router', but its value is 'undefined'. in Router
-
React Router failed prop 'history', is undefined
-
React-router v4 this.props.history.push(…) not working
-
How to push to History in React Router v4?
-
Migrating from v2/v3 to v4