reactjs

React.jsでserver-side renderingにも対応したRouting

More than 3 years have passed since last update.

前回はserver-side renderingの話をしましたが、今回はRoutingについて書きたいと思います。

React.jsはComponentを作るライブラリなので、Routerは当然含まれていません。

なのでBackbone.RouterだったりDirectorだったり好きなRouterライブラリと組み合わせて使うことが出来ます。

でもPageの単位でComponentを作って切り替える場合にそれだと面倒だったり冗長になってしまいがちです。

なのでここでは、react-routerというものを紹介したいと思います。


React Router

https://github.com/rackt/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の利用について書きたいと思います。