前回はserver-side renderingの話をしましたが、今回はRoutingについて書きたいと思います。
React.jsはComponentを作るライブラリなので、Routerは当然含まれていません。
なのでBackbone.RouterだったりDirectorだったり好きなRouterライブラリと組み合わせて使うことが出来ます。
でもPageの単位でComponentを作って切り替える場合にそれだと面倒だったり冗長になってしまいがちです。
なのでここでは、react-routerというものを紹介したいと思います。
React Router
以前まではReact Routerではserver-side renderingがサポートされていなかったので、react-router-componentというのを使っていたのですが、今はサポートされているのでserver-side renderingしたい場合にも使うことが出来ます。
React Router Componentの方がシンプルなのでわかりやすい方がいいという人はこちらを使ってみてもいいと思います。
特徴
React Router Componentもそうなのですが、RoutingをComponentとして定義する形になります。
React Routerは多機能なRouterライブラリになっていて、ネストしたRoutingのサポートやリンクにactiveなclassをつけてくれたりscrollをtopに戻してくれたりと色々やってくれます。
使い方
READMEからの引用ですが、こんな感じで定義していきます。
Routeの定義
RouteというComponentを使ってRoutingの定義を行いRouter.runで開始します。
var routes = (
<Route handler={App} path="/">
<DefaultRoute handler={Home} />
<Route name="about" handler={About} />
<Route name="users" handler={Users}>
<Route name="recent-users" path="recent" handler={RecentUsers} />
<Route name="user" path="/user/:userId" handler={User} />
<NotFoundRoute handler={UserRouteNotFound}/>
</Route>
<NotFoundRoute handler={NotFound}/>
<Redirect from="company" to="about" />
</Route>
);
Router.run(routes, function (Handler) {
React.render(<Handler/>, document.body);
});
Link
リンクはLinkというComponentがあるのでそれを用いて作ることが出来ます。
<Link to="users">Users</Link>
Handler
上記の例の場合、AppのHandlerの中にそれぞれのRouteが定義される形になるので、AppのComponentの中にRouteHandlerというComponentを定義する必要があります。
<RouteHandler />
の部分がRoutingに応じて対応するHandlerで置き換えられます。
var App = React.createClass({
render() {
return (
<div>
<h1>title</h1>
<RouteHandler />
</div>
);
};
});
History API
HTML5のHistory APIを使用したい場合はこのように第二引数に指定してRouter.runを実行します。
Router.run(routes, Router.HistoryLocation, function (Handler) {
React.render(<Handler/>, document.body);
});
server-side rendering
server-side renderingを行う場合は、server側ではこんな感じでpathを渡してRouter.runします
//express
app.use(function (req, res) {
// pass in `req.path` and the router will immediately match
Router.run(routes, req.path, function (Handler) {
var markup = React.renderToString(<Handler/>);
res.render('index', {markup: markup});
});
});
Sample
では、実際にサンプルを作ってみたのでコードを紹介していきたいと思います。
このサンプルは明日の記事にも使うのですが、VimeoとYouTubeのビデオをRoutingで切り替えられるようになっています。
初期のデータをサーバーとブラウザで共有する形になっています。
react-bootstrapとreact-videoを使っています。
curl http://react-ssr-sample.herokuapp.com/youtube
とかcurl http://react-ssr-sample.herokuapp.com/vimeo
とかすることで対応するvideoの情報がdata-react-id
と一緒に返されていることがわかると思います。
Server
一部のみの抜粋ですが、こんな感じで先ほどの例とほとんど同じです。HandlerのPropにparamsとして初期データを渡しています。
app.use(function(req, res) {
Router.run(routes, req.path, function(Handler) {
res.send(template({
initialData: JSON.stringify(data),
markup: React.renderToString(React.createElement(Handler, {params: {videos: data}}))
}));
});
});
この例では常に同じdataを返していますが、req.pathに応じたデータを返すことももちろん出来ます。
Browser entry point
server-side renderingの時にも書きましたがこんな感じでJSONを受け取ってHandlerのpropsにparamsとして渡しています。
var initialData = JSON.parse(document.getElementById('initial-data').getAttribute('data-json'));
Router.run(routes, Router.HistoryLocation, (Handler) => {
React.render(<Handler params={{videos: initialData}} />, document.getElementById("app"));
});
Routing
これは特に説明する部分もないかと思います。
module.exports = function() {
return (
<Route name="app" path="/" handler={App}>
<Route name="youtube" handler={YouTube} />
<Route name="vimeo" handler={Vimeo} />
<DefaultRoute handler={Top} />
</Route>
);
};
App
こんな感じでRouteHandlerにspread attributes({...this.props}
)で初期データをPropとして渡しています。
var App = React.createClass({
render() {
return (
<div>
<h1><Link to="app">React server-side rendering sample</Link></h1>
<ListGroup>
<Link to="youtube" key='youtube'><ListGroupItem>youtube</ListGroupItem></Link>
<Link to="vimeo" key='vimeo'><ListGroupItem>vimeo</ListGroupItem></Link>
</ListGroup>
<RouteHandler {...this.props} />
</div>
);
}
});
Handler
こんな感じでHandlerに渡した初期データをPropとして受け取ることが出来るのでそれを使って画面を描画しています。
var YouTube = React.createClass({
mixins: [VideoMixin],
render() {
return (
<Grid>
<h2>youtube</h2>
<Row className="show-grid">{this.renderVideos('youtube')}</Row>
</Grid>
);
}
});
// VideoMixin
module.exports = {
getDefaultProps() {
return {
params: {
videos: {
youtube: [],
vimeo: []
}
}
};
},
renderVideos(type) {
return this.props.params.videos[type].map( video => {
return (
<Col xs={6} md={4} key={video.id}>
<Jumbotron>
<Video from={type} id={video.id} />
<p>{video.title}</p>
</Jumbotron>
</Col>
);
});
}
};
これでserver-side renderingにも対応したRoutingを定義することが出来ました。
初期データで全部返さない場合
この例を少し変えて、/api/youtube
と/api/vimeo
でそれぞれのデータを返すようにして、/youtube
へのアクセスのときは/api/youtube
のデータを初期データとして返すようにした場合には、それぞれのComponentのcomponentDidMount
で初期データがあるか見て、ない場合にajaxリクエストを投げるといった形式にするとよいかと思います。(componentDidMount
はserver-sideでは実行されないので)
サンプル用意出来なくてすみません...。
というわけで簡単にReact Routerを使ったRoutingの書き方を紹介しました。
明日は今回紹介したような公開されているComponentの利用について書きたいと思います。