react-router@v4を使ってみよう：シンプルなtutorial

# 概要

`React`でルーティングをするためのライブラリである`react-router`のv4の基本的な使い方を覚えるために、簡単なwebページを作ってみます。

`react-router`のv4というと、v2やv3から破壊的な変更があり、かなり使いやすくなったとの評判なのですが、私はv4以前の`react-router`を使ったことはなかったので、この記事には過去のversionとの比較という観点はありません。初めて使う人が、ルーティングの概念や`react-router`の使い方がわかるチュートリアルになれば良いなと思いながら書きました。

方針としては、とにかくシンプルに小さなwebページを作ることにします。複雑な動作やサーバーとの通信はしませんし、cssもほぼ書きません。この記事は`react-router`を勉強し始めた当日に書いているので、間違っている内容があるかもしれません。ご指摘ください。

# なぜ`react-router`を使う必要があるのか

`React`でふつうにSPAを書くと、描画されるコンポーネントが変わって画面が遷移してもURLは変わらないと思います。つまり、URLとアプリの状態が関連づいていない状況になります。この２つを関連づけて、URLからアプリ内の特定の状態にアクセスできるようにしたり、逆にアプリ内での状態変化をURLに反映させたりすることをルーティングと言います。ルーティングをすることによって、ブラウザの戻るボタンが使えるようになったり、特定のURLを打ち込むことで特定のページに直接アクセスできるようになるというメリットがあります。

`react`でルーティングをするためのデファクトのライブラリが`react-router`です。

# 完成品

キャラクターに人気投票するページを作りましょう。`React`のチュートリアルでよくあるCounterの例とルーティングを組み合わせたものと考えると良いと思います。以下にアニメgifを貼っておきますが、描画しているコンテンツとURLが連動しており、またブラウザの戻る/進むボタンが機能しているのがわかると思います。

![4.gif](https://qiita-image-store.s3.amazonaws.com/0/159260/073adbb2-de6e-3466-05d5-26203358f5aa.gif)

コードとデモのサイトは以下にあります。

- [コード（github）](https://github.com/ymr-39/react-router-v4-tutorial)
- [デモ](https://yam-net.github.io/react-router-v4-tutorial)

にあります。

# 環境

- Mac OS X 10.12.3
- node 7.10.0
- npm 4.2.0
- create-react-app 1.3.1

ライブラリのバージョンは以下で示す`package.json`を見てください。

# とりあえずアプリの形をつくる

とりあえず`React`が動く環境が必要なので、簡単のため`create-react-app`を使います（使いたくない人は以下適宜読みかえてください）。

```sh
// create-react-appをインストールしていない人は
// npm i -g create-react-app

create-react-app react-router-v4-tutorial
cd react-router-v4-tutorial
```

`react-router`をインストールします。`react-router`にはいくつかのパッケージに分けられているのですが、[公式のgithub](https://github.com/ReactTraining/react-router)を見ると、ブラウザで動くページを書くには`react-router-dom`を使えばいいことがわかります。`react-router`はコアの機能を担っているのですが、[`react-router-dom`が`react-router`のexportも含めてexportしてくれる](https://github.com/ReactTraining/react-router/issues/4648)ようなので、`react-router`をインストールする必要はありません。

```sh
npm install -S react-router-dom
```

この時点で`package.json`は以下のようになっています。

```json:package.json
{
  "name": "react-router-v4-tutorial",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^15.6.1",
    "react-dom": "^15.6.1",
    "react-router-dom": "^4.1.1"
  },
  "devDependencies": {
    "react-scripts": "1.0.7"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
```

`create-react-app`で生成された余計なファイルがいっぱいあるので、いったん`src`の中のファイルをすべて消して０から書くことにしましょう。

```sh
cd src
rm *
touch index.js App.js
```

まずふつうに`index.js`を書きます。

```jsx:index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
  <App />,
  document.getElementById('root')
)
```

次に`App.js`です。今回は、すべてのコンポーネントを`App.js`の中に書くことにします。とりあえず、複雑なロジックは無視してルーティングの骨格の部分のみ書きましょう。Home, About, Friendsの3つのページを用意します。

```jsx:App.js
import React, { Component } from 'react'
import { BrowserRouter, Route, Link } from 'react-router-dom'

const App = () => (
  <BrowserRouter>
    <div>
      <Route exact path='/' component={Home} />
      <Route path='/about' component={About} />
      <Route path='/friends' component={Friends} />
    </div>
  </BrowserRouter>
)

const Home = () => (
  <div>
    <h2>Home</h2>
    <p>Welcome to ようこそ</p>
  </div>
)
const About = () => (
  <div>
    <h2>About</h2>
    <p>フレンズに投票するページです</p>
  </div>
)
const Friends = () => (
  <div>
    <h2>Friends</h2>
    <p>ここにフレンズのリストを書きます</p>
  </div>
)

export default App
```

実際にサイトを開いてみましょう。

```sh
npm start
```

して、ブラウザで`localhost:3000`にアクセスします。ここで、まずは`<Home>`コンポーネントが開き、URLを`localhost:3000/about`に変更すると`<About>`が、さらに`localhost:3000/friends`に変更すると`<Friends>`が開くことが確認できると思います。これで、URLとページに表示する内容を対応させる、基本的なルーティングができました。

このルーティングを実現しているのが、`App.js`の中で呼ばれている`<BrowserRouter>`と`<Route>`です。`<BrowserRouter>`の中にいくつか`<Route>`を書き、`path`に対応させたいURLを、`component`に描画したいコンポーネントを渡します。ここで、`exact`を設定しない限り、`path`が入力したURLに前方一致していれば描画される（つまり`localhost:3000/`以下のすべてのURLに対して`<Home>`が描画されてしまう）ため、`<Home>`には`exact`を設定しています。

もっと詳しい情報については以下のドキュメントをどうぞ。

- [`<BrowserRouter>`](https://reacttraining.com/react-router/web/api/BrowserRouter)
- [`<Route>`](https://reacttraining.com/react-router/web/api/Route)

さて、ここまでで完成したサイトはページの遷移の際にURLを直接打ち込まなければならず、明らかに不便です。それぞれのページに対するリンクを用意するために、`<App>`を書き換えましょう。

```diff:App.js
const App = () => (
  <BrowserRouter>
    <div>
+     <ul>
+       <li><Link to='/'>Home</Link></li>
+       <li><Link to='/about'>About</Link></li>
+       <li><Link to='/friends'>Friends</Link></li>
+     </ul>

+     <hr />
+     
      <Route exact path='/' component={Home} />
      <Route path='/about' component={About} />
      <Route path='/friends' component={Friends} />
    </div>
  </BrowserRouter>
)
```

`<Link>`要素の`to`に遷移させたい`path`を設定しておくことで、クリックされたとき自動で遷移してくれるようになります。これについてもドキュメントを貼っておきます。

- [`<Link>`](https://reacttraining.com/react-router/web/api/Link)

ここでページの動きを見てみましょう。

![1.gif](https://qiita-image-store.s3.amazonaws.com/0/159260/40c7b4ea-2b06-2684-073f-5407d52f91d7.gif)

リンクもうまく動いていますし、ブラウザの戻るボタンもちゃんと機能していますね。

# 2段階のルーティング

基本的なルーティングは以上で終わりです。もう少し複雑なことをしてみましょう。`friends`にアクセスしたときに、キャラクターのリストを表示して、かつそのリストからそれぞれのキャラクターのページに飛べるようにします。

まずは、表示したいキャラクターのリストを用意します。

```App.js
const FRIENDS = [
  {
    id: 'serval',
    nameJa: 'サーバル',
    nameEn: 'Serval Cat',
    family: 'ネコ目ネコ科ネコ属'
  },
  {
    id: 'raccoon',
    nameJa: 'アライグマ',
    nameEn: 'Common raccoon',
    family: 'ネコ目アライグマ科アライグマ属'
  },
  {
    id: 'fennec',
    nameJa: 'フェネック',
    nameEn: 'Fennec',
    family: 'ネコ目イヌ科キツネ属'
  }
]

const friendById = id => FRIENDS.find(friend => friend.id === id)
```

それぞれの要素に`id`を用意し、あとでこの`id`をそれぞれのキャラクターのページにアクセスする`path`として使うことにします。つまり、例えばキャラクター「サーバル」の`id`が`serval`になっているので、`localhost:3000/friends/serval`でサーバルのページに飛ぶようにするということです。また、`id`をもとにリストから対応するキャラクターのオブジェクトを探す`friendById`という関数を書いておきました。

それでは、`<Friends>`コンポーネントを書き換えます。

```diff:App.js
const Friends = () => (
  <div>
    <h2>Friends</h2>
-   <p>ここにフレンズのリストを書きます</p>
+   <Route exact path='/friends' component={FriendList} />
+   <Route path='/friends/:id' component={Friend} />
  </div>
)
```

ここで、`<Route>`で表示される`<Friends>`の中にさらにもう１段階`<Route>`を作って表示をわける必要があるのですが、上記のようにそのままふつうに書けば問題ありません。まず、正確に`localhost:3000/friends`にアクセスした場合は、`<FriendList>`を表示し、さらにその後ろにキャラクターの`id`をつけた場合、対応するキャラクターのページを表示するようにしています。

ここで、`friends/:id`のようにコロンをつけることによって、その部分に入力された文字列を変数として受け取って描画できるコンポーネント内で使えるようになります。それでは、その`id`を使う部分も含めて、`<FriendList>`と`<Friend>`を書いていきます。

```jsx:App.js
const FriendList = () => (
  <div>
    {FRIENDS.map(friend => (
      <li key={friend.id}>
        <Link to={`/friends/${friend.id}`}>{friend.nameJa}</Link>
      </li>
    ))}
  </div>
)

const Friend = props => {
  const { id } = props.match.params
  const friend = friendById(id)

  if (typeof friend === 'undefined')  {
    return (
      <div>
        <p>Friends with id '{id}' does not exist.</p>
      </div>
    )
  }


  const containerStyle = { border: '1px gray solid', display: 'inline-block', padding: 10 }
  const contentsStyle = { margin: 0 }

  return (
    <div>
      <div style={containerStyle}>
        <p style={contentsStyle}>{friend.family}</p>
        <h1 style={contentsStyle}>{friend.nameJa}</h1>
        <p style={contentsStyle}>{friend.nameEn}</p>
      </div>
    </div>
  )
}
```

まず、`<FriendList>`では、配列`FRIENDS`を`map`して、それぞれのキャラクターのページへの`<Link>`を貼っています。

`<Friend>`の動作は少し複雑です。まず、このコンポーネントでは`props`を受け取っています。`<Friends>`のコードを見ると、`<Friend>`はなにも`props`を受け取っていないように見えるのですが、実はすべての`<Route>`は自動的にいくつかの`props`を受け取ります。

- [Route props](https://reacttraining.com/react-router/web/api/Route/Route-props)

今回は、その中の`match`を使います。実は、`match.params.id`の中に、`<Friends>`内で`<Friend>`にアクセスするときの`path`の`/friends/:id`の`:id`の部分の文字列が入っているのです。

そのため、この受け取った`id`をもとに`friend`のオブジェクトを探し出し、それを描画に使うことで対応するキャラクターの画面を表示することができます。

たとえば`/friends/serval`にアクセスしたときには、`<Friend>`内での`id`は`serval`になり、`friendById`によって`friend`には配列`FRIENDS`の要素である以下のオブジェクトが入ることになります。

```js
{
  id: 'serval',
  nameJa: 'サーバル',
  nameEn: 'Serval Cat',
  family: 'ネコ目ネコ科ネコ属'
}
```

コードを見ると、`render()`の内部で、上記のオブジェクトがうまく描画に使われていることがわかると思います。

また、

```jsx:App.js
if (typeof friend === 'undefined')  {
  return (
    <div>
      <p>Friends with id '{id}' does not exist.</p>
    </div>
  )
}
```

の部分は存在しない`id`にアクセスされたときのためのエラー処理です。例えば、ブラウザで`localhost:3000/dog`にアクセスすると、`FRIENDS`内に`id`が`dog`の要素がないことから`friend`が`undefined`になってしまうので、その際に別の画面を表示するようにしています。

それでは、ページを見てみましょう。

![2.gif](https://qiita-image-store.s3.amazonaws.com/0/159260/5ab9b49d-5ee2-1f81-796d-0a9c23892d6d.gif)

うまく動いています。

# Reactの機能を使う

ここで終わっても良いのですが、冷静に考えてここまでは`React`の機能をなにも使っておらず、単にルーティングをして静的なコンポーネントを返しているだけです。せっかくなので、`React`を使って動的なページにしてみましょう。

今回は、`<FriendList>`からキャラクターに投票できるようにして、各`<Friend>`のページで投票数を表示するようにします。

まずは、`<Friends>`以下を書き換えます。ここで、`<Friends>`に投票数の状態を持たせたいため、既存の`<Friends>`を消して、関数ではなくクラスとして０から書き直すことにします。

```jsx:App.js
class Friends extends Component {
  constructor() {
    super()
    this.state = {}
    this.handleVote = this.handleVote.bind(this)
  }

  componentWillMount() {
    FRIENDS.forEach(friend => {
      this.setState({
        ...this.state,
        [friend.id]: 0
      })
    })
  }

  handleVote(id) {
    this.setState({
      [id]: this.state[id] + 1
    })
  }

  render() {
    return (
      <div>
        <h2>Friends</h2>
        <Route exact path='/friends' render={props => <FriendList handleVote={this.handleVote} />} />
        <Route path='/friends/:id' render={props => <Friend match={props.match} votes={this.state} />} />
      </div>
    )
  }
}
```

かなり大きな変更があります。まず、`constructor()`と`componentWillMount()`内で`state`を初期化しています。とくに`componentWillMount()`内は複雑なコードに見えますが、冷静に見てみると、単に`state`を

```jsx
this.state = {
  serval: 0,
  raccoon: 0,
  fennec: 0
}
```

としているのと同じことです（最初からそう書けという気もしますが、今後`FRIENDS`が増えてもコードの変更をしなくて良いようにしています）。この`state`は、各キャラクターへの投票数を表しています。また、イベントハンドラの`handleVote`で、投票に応じて`state`において対応するキャラクターの投票数を１増やすようにしています。

さて、投票のボタンを`<FriendList>`に置いて、投票数を`<Friend>`内に表示したいので、それぞれ`handleVote`を`<FriendList>`に、`this.state`を`<Friend>`に渡したいです。ただ、上でも述べたように`<Route>`には、`match`などデフォルトで渡される`props`があり、追加の`props`を渡すことができないようになっています（たぶん）。そのため、`<Route>`を、

```diff:App.js
- <Route exact path='/friends' component={FriendList} />
- <Route path='/friends/:id' component={Friend} />
+ <Route exact path='/members' render={props => <FriendList handleVote={this.handleVote} />} />
+ <Route path='/members/:id' render={props => <Friend match={props.match} votes={this.state} />} />
```

のように書き換えています。実は、`<Route>`に描画するコンポーネントを渡すには`component`以外にも、`render`、`children`という方法があり、ここでは`render`を使っています。

`render`には、「`match`などが含まれるデフォルトの`props`を引数にとって、描画したいコンポーネントを返す関数」を渡すことにより描画をします。`<FriendList>`ではデフォルトの`props`を使っていないことから`handleVote`のみを渡し、`<Friend>`では`match`を使うので`match`と投票数`this.state`を`votes`として渡しています。

それでは、`<FriendList>`と`<Friend>`を見ていきましょう。これらは、渡された`props`をうまく使ってあげれば良いだけなので小さな変更です。

```diff:App.js
- const FriendList = () => (
+ const FriendList = props => (
  <div>
    {FRIENDS.map(friend => (
      <li key={fiend.id}>
        <Link to={`/friends/${friend.id}`}>{friend.nameJa}</Link>
+       <button onClick={() => props.handleVote(friend.id)}>+</button>
      </li>
    ))}
  </div>
)

const Friend = props => {
  const { id } = props.match.params
  const friend = friendById(id)
+ const vote = props.votes[id]

  if (typeof friend === 'undefined')  {
    return (
      <div>
        <p>Friends with id '{id}' does not exist.</p>
      </div>
    )
  }

  const containerStyle = { border: '1px gray solid', display: 'inline-block', padding: 10 }
  const contentsStyle = { margin: 0 }

  return (
    <div>
      <div style={containerStyle}>
        <p style={contentsStyle}>{friend.family}</p>
        <h1 style={contentsStyle}>{friend.nameJa}</h1>
        <p style={contentsStyle}>{friend.nameEn}</p>
      </div>
+     <h1>Vote: {vote}</h1>
    </div>
  )
}
```

それでは、ページの動きを見てみます。

![3.gif](https://qiita-image-store.s3.amazonaws.com/0/159260/6b7b9900-0c53-c650-6c94-e428966b06eb.gif)

ということで、無事投票ができています。以上でこのページは完成とします。

※今回は、簡単のため投票数を`<Friends>`コンポーネントで管理していますが、これだと`<FriendList>`で投票してから一旦違うページ（例えば`<About>`）に飛ぶと投票数の情報が失われてしまいます。この問題は、情報をさらに持ち上げて`<App>`の`state`で管理するか、`redux`を使って`store`で管理するなどにより解決します。

# 今回使っていないAPI

`react-router`の機能のうち、今回使っていないものをいくつか紹介します。それぞれについて、公式のドキュメントと使用例を貼っていきます。

## `<Route children={...}>`

`<Route>`にコンポーネントを渡す方法で`component`と`render`を紹介しましたが、もう一つ`children`というやり方があります。使い方は`render`と同じのようですが、こちらは`<Route>`がURLにmatchしないとき、`props.match`が`null`になるという違いがあるようです。

- [`<Route>` children: func](https://reacttraining.com/react-router/web/api/Route/children-func)

## `<NavLink>`

`<NavLink>`は`<Link>`と同じ働きをしますが、描画されているコンポーネントに対応するリンクにスタイリングをすることができます。今回作ったページでも、`<App>`の中の`<Link>`を

```jsx
<ul>
  <li><NavLink activeStyle={{ color: 'red' }} exact to='/'>Home</NavLink></li>
  <li><NavLink activeStyle={{ color: 'red' }} to='/about'>About</NavLink></li>
  <li><NavLink activeStyle={{ color: 'red' }} to='/friends'>Friends</NavLink></li>
</ul>
```

のように変更するだけで、現在のURLに対応するリンクをスタイリングしてくれます。

- [`<NavLink>`](https://reacttraining.com/react-router/web/api/NavLink)

## `<Redirect>`

例えばログインしていないユーザーとログインしているユーザーで別のコンテンツを描画したいときとかに使えるっぽいです。

- [`<Redirect>`](https://reacttraining.com/react-router/web/api/Redirect)
- [Redirect Auth example](https://reacttraining.com/react-router/web/example/auth-workflow)

## `<Switch>`

今回は複数の`<Route>`を並べるとき単に

```jsx
<Route ...>
<Route ...>
<Route ...>
```

と書いていましたが、これを`<Switch>`を使って

```jsx
<Switch>
  <Route ...>
  <Route ...>
  <Route ...>
</Switch>
```

と書くことができます。これによって、`<Route>`が排他的に描画されるようになります。すなわち、URLが複数の`<Route>`にmatchしている場合でも、ただ一つだけ一番上の`<Route>`が描画されます。これによって、意図せず複数の`<Route>`が描画されてしまうのを防げることに加えて、以下の例のようにmatchする`<Route>`がない場合のデフォルトの`<Route>`を作れたり、`path`中のパラメータを使った柔軟なルーティングができるようになります。

- [`<Switch>`](https://reacttraining.com/react-router/web/api/Switch)
- [No Match example](https://reacttraining.com/react-router/web/example/no-match)
- [Ambiguous Match](https://reacttraining.com/react-router/web/example/ambiguous-matches)

# `redux`との併用

`React`を使うとなると、`redux`も使いたい場合がほとんどだと思うんですが、下記のドキュメントによるとけっこうややこしいっぽいです。

- [Redux integration](https://reacttraining.com/react-router/web/guides/redux-integration)

正直あまりよくわからなかったのでいろいろと不正確な可能性が高いですが、一応まとめると、

だいたいの場合は単に`redux`を使ってもうまくいく（その場合`redux`の`store`にはルーティングの情報はなく、それ以外のデータのみ管理することになる（？））。しかし、

- `connect`を使う場合
- `<Route component={Component}>`以外の形で描画する場合

はうまくいかない（ルーティングの情報が変わっても、`store`が変わらない限り`shouldComponentUpdate`が呼ばれないから）。ただ、それらの場合にも以下のよう`withRouter`を使ってあげればうまく動く。

```jsx
// NG
export default connect(mapStateToProps)(Something)

// OK
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))
```

ただし、上記のOKの場合でも、以下のようなことはできない。

- `store`とルーティングのデータを同期したり、`store`からルーティングを操作したりする
- `dispatch`によってページ遷移をする
- ページ遷移についてもRedux devtoolのtime travel debuggingをする

どうしても上記のようなことをしたい場合は、[`react-router-redux`](https://github.com/reacttraining/react-router/tree/master/packages/react-router-redux)を使えば良い。

とのことです。間違っているところをご指摘いただけるとありがたいです。とりあえずよくわからないので、気が向いたら`redux`との組み合わせについても調べたいと思います。

以上、よろしくお願いします。