概要
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とアプリの状態が関連づいていない状況になります。この2つを関連づけて、URLからアプリ内の特定の状態にアクセスできるようにしたり、逆にアプリ内での状態変化をURLに反映させたりすることをルーティングと言います。ルーティングをすることによって、ブラウザの戻るボタンが使えるようになったり、特定のURLを打ち込むことで特定のページに直接アクセスできるようになるというメリットがあります。
react
でルーティングをするためのデファクトのライブラリがreact-router
です。
完成品
キャラクターに人気投票するページを作りましょう。React
のチュートリアルでよくあるCounterの例とルーティングを組み合わせたものと考えると良いと思います。以下にアニメgifを貼っておきますが、描画しているコンテンツとURLが連動しており、またブラウザの戻る/進むボタンが機能しているのがわかると思います。
コードとデモのサイトは以下にあります。
にあります。
環境
- 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
を使います(使いたくない人は以下適宜読みかえてください)。
// 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を見ると、ブラウザで動くページを書くにはreact-router-dom
を使えばいいことがわかります。react-router
はコアの機能を担っているのですが、react-router-dom
がreact-router
のexportも含めてexportしてくれるようなので、react-router
をインストールする必要はありません。
npm install -S react-router-dom
この時点で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
の中のファイルをすべて消して0から書くことにしましょう。
cd src
rm *
touch index.js App.js
まずふつうに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つのページを用意します。
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
実際にサイトを開いてみましょう。
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
を設定しています。
もっと詳しい情報については以下のドキュメントをどうぞ。
さて、ここまでで完成したサイトはページの遷移の際にURLを直接打ち込まなければならず、明らかに不便です。それぞれのページに対するリンクを用意するために、<App>
を書き換えましょう。
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
を設定しておくことで、クリックされたとき自動で遷移してくれるようになります。これについてもドキュメントを貼っておきます。
ここでページの動きを見てみましょう。
リンクもうまく動いていますし、ブラウザの戻るボタンもちゃんと機能していますね。
2段階のルーティング
基本的なルーティングは以上で終わりです。もう少し複雑なことをしてみましょう。friends
にアクセスしたときに、キャラクターのリストを表示して、かつそのリストからそれぞれのキャラクターのページに飛べるようにします。
まずは、表示したいキャラクターのリストを用意します。
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>
コンポーネントを書き換えます。
const Friends = () => (
<div>
<h2>Friends</h2>
- <p>ここにフレンズのリストを書きます</p>
+ <Route exact path='/friends' component={FriendList} />
+ <Route path='/friends/:id' component={Friend} />
</div>
)
ここで、<Route>
で表示される<Friends>
の中にさらにもう1段階<Route>
を作って表示をわける必要があるのですが、上記のようにそのままふつうに書けば問題ありません。まず、正確にlocalhost:3000/friends
にアクセスした場合は、<FriendList>
を表示し、さらにその後ろにキャラクターのid
をつけた場合、対応するキャラクターのページを表示するようにしています。
ここで、friends/:id
のようにコロンをつけることによって、その部分に入力された文字列を変数として受け取って描画できるコンポーネント内で使えるようになります。それでは、そのid
を使う部分も含めて、<FriendList>
と<Friend>
を書いていきます。
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
を受け取ります。
今回は、その中のmatch
を使います。実は、match.params.id
の中に、<Friends>
内で<Friend>
にアクセスするときのpath
の/friends/:id
の:id
の部分の文字列が入っているのです。
そのため、この受け取ったid
をもとにfriend
のオブジェクトを探し出し、それを描画に使うことで対応するキャラクターの画面を表示することができます。
たとえば/friends/serval
にアクセスしたときには、<Friend>
内でのid
はserval
になり、friendById
によってfriend
には配列FRIENDS
の要素である以下のオブジェクトが入ることになります。
{
id: 'serval',
nameJa: 'サーバル',
nameEn: 'Serval Cat',
family: 'ネコ目ネコ科ネコ属'
}
コードを見ると、render()
の内部で、上記のオブジェクトがうまく描画に使われていることがわかると思います。
また、
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
になってしまうので、その際に別の画面を表示するようにしています。
それでは、ページを見てみましょう。
うまく動いています。
Reactの機能を使う
ここで終わっても良いのですが、冷静に考えてここまではReact
の機能をなにも使っておらず、単にルーティングをして静的なコンポーネントを返しているだけです。せっかくなので、React
を使って動的なページにしてみましょう。
今回は、<FriendList>
からキャラクターに投票できるようにして、各<Friend>
のページで投票数を表示するようにします。
まずは、<Friends>
以下を書き換えます。ここで、<Friends>
に投票数の状態を持たせたいため、既存の<Friends>
を消して、関数ではなくクラスとして0から書き直すことにします。
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
を
this.state = {
serval: 0,
raccoon: 0,
fennec: 0
}
としているのと同じことです(最初からそう書けという気もしますが、今後FRIENDS
が増えてもコードの変更をしなくて良いようにしています)。このstate
は、各キャラクターへの投票数を表しています。また、イベントハンドラのhandleVote
で、投票に応じてstate
において対応するキャラクターの投票数を1増やすようにしています。
さて、投票のボタンを<FriendList>
に置いて、投票数を<Friend>
内に表示したいので、それぞれhandleVote
を<FriendList>
に、this.state
を<Friend>
に渡したいです。ただ、上でも述べたように<Route>
には、match
などデフォルトで渡されるprops
があり、追加のprops
を渡すことができないようになっています(たぶん)。そのため、<Route>
を、
- <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
をうまく使ってあげれば良いだけなので小さな変更です。
- 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>
)
}
それでは、ページの動きを見てみます。
ということで、無事投票ができています。以上でこのページは完成とします。
※今回は、簡単のため投票数を<Friends>
コンポーネントで管理していますが、これだと<FriendList>
で投票してから一旦違うページ(例えば<About>
)に飛ぶと投票数の情報が失われてしまいます。この問題は、情報をさらに持ち上げて<App>
のstate
で管理するか、redux
を使ってstore
で管理するなどにより解決します。
今回使っていないAPI
react-router
の機能のうち、今回使っていないものをいくつか紹介します。それぞれについて、公式のドキュメントと使用例を貼っていきます。
<Route children={...}>
<Route>
にコンポーネントを渡す方法でcomponent
とrender
を紹介しましたが、もう一つchildren
というやり方があります。使い方はrender
と同じのようですが、こちらは<Route>
がURLにmatchしないとき、props.match
がnull
になるという違いがあるようです。
<NavLink>
<NavLink>
は<Link>
と同じ働きをしますが、描画されているコンポーネントに対応するリンクにスタイリングをすることができます。今回作ったページでも、<App>
の中の<Link>
を
<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に対応するリンクをスタイリングしてくれます。
<Redirect>
例えばログインしていないユーザーとログインしているユーザーで別のコンテンツを描画したいときとかに使えるっぽいです。
<Switch>
今回は複数の<Route>
を並べるとき単に
<Route ...>
<Route ...>
<Route ...>
と書いていましたが、これを<Switch>
を使って
<Switch>
<Route ...>
<Route ...>
<Route ...>
</Switch>
と書くことができます。これによって、<Route>
が排他的に描画されるようになります。すなわち、URLが複数の<Route>
にmatchしている場合でも、ただ一つだけ一番上の<Route>
が描画されます。これによって、意図せず複数の<Route>
が描画されてしまうのを防げることに加えて、以下の例のようにmatchする<Route>
がない場合のデフォルトの<Route>
を作れたり、path
中のパラメータを使った柔軟なルーティングができるようになります。
redux
との併用
React
を使うとなると、redux
も使いたい場合がほとんどだと思うんですが、下記のドキュメントによるとけっこうややこしいっぽいです。
正直あまりよくわからなかったのでいろいろと不正確な可能性が高いですが、一応まとめると、
だいたいの場合は単にredux
を使ってもうまくいく(その場合redux
のstore
にはルーティングの情報はなく、それ以外のデータのみ管理することになる(?))。しかし、
-
connect
を使う場合 -
<Route component={Component}>
以外の形で描画する場合
はうまくいかない(ルーティングの情報が変わっても、store
が変わらない限りshouldComponentUpdate
が呼ばれないから)。ただ、それらの場合にも以下のようwithRouter
を使ってあげればうまく動く。
// 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
を使えば良い。
とのことです。間違っているところをご指摘いただけるとありがたいです。とりあえずよくわからないので、気が向いたらredux
との組み合わせについても調べたいと思います。
以上、よろしくお願いします。