サンプルでreact-router v4を理解してみよう。


v3からv4への変更点

react-router v4については日本語の資料が少なかったので、

ReactTrainingのgithubのreadme.mdと英語で書かれたブログを参照した。

react-router v3も使ったことがなかったので、変更点を読んでも理解できず、かなり時間を使った気がする。

ようやく理解できたと思うので、整理してみた。

最後には簡単なサンプルも一緒に紹介する。

まずv3からv4への主な変更点をまとめた。

(この記事はReact Router v4 Unofficial Migration Guideという記事をかなり引用している。)


  • react-routerからreact-router-domとreact-router-nativeに分岐

  • <Switch>の追加

  • routeにexactを追加

  • nested routeはもう使えない。

  • <IndexRoute>はもう使えない。

  • paramsの代わりにmatch.paramsを使う。

  • query stringを使う場合はquery-stringを使う。

  • onEnterの代わりにcomponentWillMount又はcomponentWillReceivePropsを使う。

  • Redux統合


react-routerからreact-router-domとreact-router-nativeに分岐

react-routerはCoreの役割を担う。

ウェブで開発する場合はreact-router-domを、アプリで開発する場合はreact-router-nativeを使う。

react-router-domを設置するとき、依存性パッケージとしてreact-routerも一緒に設置される。


<Switch>の追加

v3で<Route>は排他的(exclusive)で、urlにマッチされた最初の<Route>だけレンダリングされるが、v4で<Route>はもう排他的ではない。

urlにマッチされる<Route>はすべてレンダリングされる。

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

const MyApp = () => (
<Router history={history}>
<Route path="/posts" component={PostList} />
<Route path="/posts/:id" component={PostEdit} />
<Route path="/posts/:id/show" component={PostShow} />
<Route path="/posts/:id/delete" component={PostDelete} />
</Router>
);

v4ではurlが"/posts/12/show"の場合、"/posts"と"/posts/:id/show"がレンダリングされる。

v3と同様に使いたい場合は、<Switch>を使う。

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const MyApp = () => (
<Router history={history}>
<Switch>
<Route path="/posts" component={PostList} />
<Route path="/posts/:id" component={PostEdit} />
<Route path="/posts/:id/show" component={PostShow} />
<Route path="/posts/:id/delete" component={PostDelete} />
</Switch>
</Router>
);

urlが"/posts/12/show”でも最初の"/posts"だけがレンダリングされる。


<Route>にexactを追加

しかし、これだけでは十分ではない。

urlが"/posts/12/show"の場合、"/posts"がマッチされてしまう。

v3ライクな振る舞いを実現したい場合は、<Route>にexactというpropを追加する。

exactは「正確な」という意味だ。

exact pathだから正確なパスという意味になる。

urlが"/posts/12/show"の場合、"/posts"ではない"/posts/:id/show"がレンダリングされる。

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const MyApp = () => (
<Router history={history}>
<Switch>
<Route exact path="/posts" component={PostList} />
<Route exact path="/posts/:id" component={PostEdit} />
<Route exact path="/posts/:id/show" component={PostShow} />
<Route exact path="/posts/:id/delete" component={PostDelete} />
</Switch>
</Router>
);


nested Routeはもう使えない。

v3ではnested Routeを使えたが、v4では使えない。

// in src/MyApp.js

const MyApp = () => (
<Router history={history}>
<Route path="/main" component={Layout}>
<Route path="/foo" component={Foo} />
<Route path="/bar" component={Bar} />
</Route>
</Router>
);

// in src/Layout.js
const Layout = ({ children }) => (
<div className="body">
<h1 className="title">MyApp</h1>
<div className="content">
{children}
</div>
</div>
);

Layout componentの{children}はchild Route componentに切り替える。(foo又はbar)

v4ではnested Routeは使えないので、child componentの中にnested Routeを記入する。

確かに該当するcomponentの中にRoute情報も一緒にあったほうがメンテナンスしやすい。

// in src/MyApp.js

const MyApp = () => (
<Router history={history}>
<Route path="/main" component={Layout} />
</Router>
);

// in src/Layout.js
const Layout = () => (
<div className="body">
<h1 className="title">MyApp</h1>
<div className="content">
<Switch>
<Route path="/main/foo" component={Foo} />
<Route path="/main/bar" component={Bar} />
</Switch>
</div>
</div>
);

ここでpathは絶対パスでなければならない。top-levelのpathを使いたくない場合は、match propを使ってpathを再構成する。

// in src/Layout.js

const Layout = ({ match }) => (
<div className="body">
<h1 className="title">MyApp</h1>
<div className="content">
<Switch>
<Route path={`${match.url}/foo`} component={Foo} />
<Route path={`${match.url}/bar`} component={Bar} />
</Switch>
</div>
</div>
);


<IndexRoute>はもう使えない。

IndexRoute componentはv4ではもう存在しない。

v4では<Switch>, exact, そして<Route>の順序を組み合わせて使う。

// in src/MyApp.js

const MyApp = () => (
<Router history={history}>
<Route path="/" component={Layout} />
</Router>
);

// in src/Layout.js
const Layout = () => (
<div className="body">
<h1 className="title">MyApp</h1>
<div className="content">
<Switch>
<Route exact path="/foo" component={Foo} />
<Route exact path="/bar" component={Bar} />
<Route exact path="/" component={Dashboard} />
</Switch>
</div>
</div>
);


paramsの代わりにmatch.paramsを使う。

v3では次のように使えていた。

// in src/MyApp.js

const MyApp = () => (
<Router history={history}>
<Route path="/posts/:id" component={PostEdit} />
</Router>
);

// in src/PostEdit.js
const PostEdit = ({ params }) => (
<div>
<h1>Post #{params.id}</h1>
...
</div>
);

v4ではmatch.paramsを使う。

// v4

const PostEdit = ({ match }) => (
<div>
<h1>Post #{match.params.id}</h1>
...
</div>
)


query stringを使う場合はquery-stringを使う。

v3ではデフォルトでquery stringをparseしていた。

location.queryにquery parameterを格納していた。

例えば、"/posts?sort=foo"の場合は次のように使えた。

// in src/PostList.js

const PostList = ({ location }) => (
<div>
<h1>List sorted by {location.query.sort}</h1>
</div>
);

しかし、v4ではlocation.queryは存在しない。

そして、query stringはparseしない。

手動でlocation.searchをparseしなければならない。

query-stringのようなthird-party libraryを利用する。

import { parse } from 'query-string';

const PostList = ({ location }) => {
const query = parse(location.search);
return (
<div>
<h1>List sorted by {query.sort}</h1>
</div>
);
};


onEnterの代わりにcomponentWillMount又はcomponentWillReceivePropsを使う。

onEnter propは普段毎Routeの前にユーザーの資格を証明するために使っていた。

// v3

// in src/MyApp.js
import checkCredentials from '../checkCredentials';
function redirectToLoginIfNotAuthenticated = (nextState, replace, callback) =>
checkCredentials()
.then(callback)
.catch(e => replace({ pathname: '/login' })
<Route onEnter={redirectToLoginIfNotAuthenticated} component={PostList} />

v4ではonEnterはもう存在しない。

Routeをレンダリングする前にチェックするためには、componentWillMount又はcomponentWillReceivePropsを使う。

// v4

// in src/PostList.js
import { withRouter } from 'react-router-dom';
import checkCredentials from '../checkCredentials';
class PostList extends Component {
componentWillMount() {
this.checkAuthentication(this.props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.location !== this.props.location) {
this.checkAuthentication(nextProps);
}
}
checkAuthentication(params) {
const { history } = params;
checkCredentials()
.catch(e => history.replace({ pathname: '/login' }));
}
render() {
// ...
}
}
export withRouter(PostList);
// in src/MyApp.js
<Route component={PostList} />


Redux統合

Reduxを使っていたとしたら、reactjs/react-router-reduxを使っていたかもしれない。

しかし、このライブラリはもうメンテナンスされない。

react-router-reduxのメンテナンスはreact-training organizationに移管された。

これからはreact router v4を使う。


サンプル

このサンプルはpro reactという本のreact router関連サンプルをv4で直したものだ。

本を読んだ人は分かると思うが、同じなのはレンダリングされた画面の結果だけだ。

ES6で書かれているが2015年度に出版された本なので、最新ライブラリを用いて再作成するためにはかなり時間を要する。

社内でreactの勉強会のために作成した記事にはAirbnbのスタイルガイドと最新ライブラリに合わせてソースコードを再作成した。

本の内容は気に入るが古いから購入を躊躇した人にこの記事が役に立つことを願う。

先に完成された画面をみよう。

react-router.gif

肝心なのは著者のgithub repositoryの一覧を表示するところだ。

一覧のリンクをクリックすると一覧の下に該当リポジトリの詳細を表示する。

画面では見えないが、リンクをクリックする際にちゃんとurlが切り替える。

さあ、始めてみよう。

まずはEntry Pointになるファイルを作成する。

v4でRouterはもう使えない。代わりにBrowserRouterを使う。

(BrowserRouter as Routerを使ってもいいと思う。)


src/index.js

import React from 'react';

import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './components/routing/App';
import registerServiceWorker from './registerServiceWorker';
import './index.css';

render((
<BrowserRouter>
<App />
</BrowserRouter>
), document.getElementById('root'));
registerServiceWorker();


次はcssファイルの内容だ。


src/index.css

body {

margin: 0;
font: 16px/1 sans-serif;
}
menu ul {
margin: 0;
padding: 0;
}
menu li {
display: inline-block;
padding: 5px;
}
a.active {
color: #444;
font-weight: bold;
text-decoration: none;
}
header {
padding: 10px;
background-color: #333;
color: #ccc;
font-size: 20px;
font-weight: bold;
}
menu {
background-color: #ccc;
padding: 5px;
margin-top: 0;
margin-bottom: 10px;
}

これでサンプルアプリケーションのEntry Pointが用意された。

次はレイアウトファイルの出番だ。

Headerと本文をレンダリングするMainで構成されている。


src/components/routing/App.js

import React from 'react';

import Header from './Header';
import Main from './Main';

const App = () => (
<div>
<Header />
<Main />
</div>
);

export default App;


Headerの内容である。


src/components/routing/Header.js

import React from 'react';

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

const Header = () => (
<div>
<header>App</header>
<menu>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/repos">Repos</Link></li>
</ul>
</menu>
</div>
);

export default Header;


Mainの内容である。

RouteのグループをSwitchで囲んでいるので、Routeの挙動は排他的だ。


src/components/routing/Main.js

import React from 'react';

import { Switch, Route } from 'react-router-dom';
import About from './About';
import Home from './Home';
import Repos from './Repos';

const Main = () => (
<main>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/repos" component={Repos} />
</Switch>
</main>
);

export default Main;


Homeの画面を表すpure componentである。


src/components/routing/Home.js

import React, { Component } from 'react';

class Home extends Component {
render() {
return (
<h1>HOME</h1>
);
}
}

export default Home;


Aboutの画面を表すpure componentである。


src/components/routing/About.js

import React, { Component } from 'react';

class About extends Component {
render() {
return (
<h1>About</h1>
);
}
}

export default About;


Reposの画面を表すLayout componentである。

著者のgithubのリポジトリの一覧と個別リポジトリの詳細を表すcomponentで構成されている。

<Switch>を使っていないので、マッチされた<Route>はすべてレンダリングされる。

<Switch>で<Route>を囲む場合はリンクをクリックしても一番最初にマッチした"/repos"だけがレンダリングされる。


src/components/routing/Repos.js

import React from 'react';

import { Route } from 'react-router-dom';
import RepoList from './RepoList';
import RepoDetails from './RepoDetails';

const Repos = () => (
<div>
<Route path="/repos" component={RepoList} />
<Route path="/repos/details/:name" component={RepoDetails} />
</div>
);

export default Repos;


リポジトリ一覧を表すcomponentである。


src/components/routing/RepoList.js

import React, { Component } from 'react';

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

class RepoList extends Component {
constructor(props) {
super(props);
this.state = {
repositories: [],
};
}

componentDidMount() {
fetch('https://api.github.com/users/pro-react/repos')
.then(response => response.json())
.then((responseData) => {
this.setState({ repositories: responseData });
});
}

render() {
const repos = this.state.repositories.map(repo => (
<li key={repo.id}>
<Link to={`/repos/details/${repo.name}`}>{repo.name}</Link>
</li>
));
return (
<div>
<h1>Github Repos</h1>
<ul>
{repos}
</ul>
</div>
);
}
}

export default RepoList;


リポジトリの詳細をレンダリングする部分だ。

nameパラメータ(リポジトリ名)を取得するためにmatch.paramsを使っている。

nameパラメータを用いてgithubのapiと通信して必要なデータを取得する。


src/components/routing/RepoDetails.js

import React, { Component } from 'react';

import PropTypes from 'prop-types';
import 'whatwg-fetch';

class RepoDetails extends Component {
constructor(props) {
super(props);
this.state = {
repository: {},
};
}

componentDidMount() {
const name = this.props.match.params.name;
this.fetchData(name);
}

componentWillReceiveProps(nextProps) {
const name = nextProps.match.params.name;
this.fetchData(name);
}

fetchData(name) {
fetch(`https://api.github.com/repos/pro-react/${name}`)
.then(response => response.json())
.then((responseData) => {
this.setState({ repository: responseData });
});
}

render() {
const stars = [];
for (let i = 0; i < this.state.repository.stargazers_count; i += 1) {
stars.push('★');
}

return (
<div>
<h2>{ this.state.repository.name }</h2>
<p>{ this.state.repository.description }</p>
<span>{ stars }</span>
</div>
);
}
}

RepoDetails.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
name: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
};

export default RepoDetails;



参考