この記事は React Advent Calendar 2016 の 6 日目の記事です。
(アドベントカレンダーに紐づけるの忘れたまま日付超えてしまいました……ごめんなさい!)
react-router v4
が良さそうという話を聞き、flux/utils
で作られたアプリケーションを書き換えたので、特徴を簡単に説明します。
TL;DR
1. はじめに
react
をある程度知っている人向けになります。ごめんなさい!
react-router
/ flux
については少し知っていれば大丈夫な気もします。
react-router v4
自体については以下から学ばせて頂きました
Hot Module Replacement
については以下が詳しいです
2. React Router v4 での色々な書き方
上で紹介した投稿にもありますが、React Router はかなり大きく、その書き方に縛られる部分が結構あるように感じられました。
v4 では props.children
を使って子 Component のレンダリング場所を指定したり、画面の遷移にhistory
を渡す必要があったり、というのがなくなり、素直に書けるようになっているように感じられます。
ただし、現状ではドキュメントやサンプルとなる資料が少なかったので、いくつかの実装例をコードを交えて説明します。
共通部分(ナビゲーションバー/サイドバー)
react-router v4
では以下のように素直に書けます。
1<BrowserRouter>
2 <div>
3 <nav className="navbar navbar-inverse">
4 <div className="container">
5 <Link className="navbar-brand" to="/">SampleApp</Link>
6 <ul className="nav navbar-nav">
7 <li><Link to="/menu1">Menu1</Link></li>
8 <li><Link to="/menu2">Menu2</Link></li>
9 <li><Link to="/menu3">Menu3</Link></li>
10 </ul>
11 </div>
12 </nav>
13 <div className="container">
14 <Match exactly pattern="/" component={Top} />
15 <Match exactly pattern="/menu1" component={Menu1} />
16 <Match exactly pattern="/menu2" component={Menu2} />
17 <Match exactly pattern="/menu3" component={Menu3} />
18 </div>
19 </div>
20</BrowserRouter>
これでナビゲーションバーはそのままに、内部(ここでは .container
の div
の内側)だけが差し変わる形式になります。
親子関係を Route
に記述して props.children
として渡していた v3 に比べると、より宣言的、直観的に書けるようになっていると思います。
flux の Container
を作る
BrowserRouter
の中は Stateless Functional Component
でなければならないため、BrowserRouter
の外で定義します。
1class Root extends React.Component {
2 constructor(props) {
3 super(props);
4 }
5 static getStores() {
6 return [
7 BookStore,
8 ];
9 }
10 static calculateState() {
11 return {
12 appState: {
13 books: BookStore.getState()
14 },
15 };
16 }
17
18 render() {
19 return (
20 <BrowserRouter>
21 <div className="container">
22 <Match exactly pattern="/menu1" component={Menu1} />
23 <Match exactly pattern="/menu2" component={Menu2} />
24 <Match exactly pattern="/menu3" component={Menu3} />
25 </div>
26 </BrowserRouter>
27 );
28 }
29}
30
31const App = Container.create(Root);
32export default App;
props を渡す
flux
を使っていると、アプリケーション全体の state
を子要素に渡す実装になると思います。
公式のドキュメントには props
を渡す方法についてのわかりやすい記述がありませんが、以下のように render
を使うことで実装できます。
1<Match
2 exactly pattern="/books"
3 render={() => <BookList appState={this.state.appState} />}
4/>
path パラメータを取得する
URL から path パラメータを取得するには、 Match
の pattern
に /:id
といった形で指定します。
params
を props
として渡すため上記の通り render
を使用します。
render
に渡す匿名関数に渡される値の中の params
を渡すことで実装できます。
匿名関数に渡される値は params
の他に、 isExact
location
pathname
pattern
が取得できます。
1<Match
2 pattern="/books/detail/:id"
3 render={({params}) => <BookDetail appState={this.state.appState} params={params} />}
4/>
以下の用に渡した先の Component
から取得できます。
1const book =
2 this.props.appState.books.find((b) => b.id === +this.props.params.id) || {};
js の処理内で画面を遷移させる
BrowserRouter
直下にある router
を遷移処理を行いたい Component
まで渡します。
1<BrowserRouter>
2 {({ router }) => (
3 <div className="container">
4 <Match
5 pattern="/books/new"
6 render={() => <BookCreate appState={this.state.appState} router={router} />}
7 />
8 </div>
9 )}
10</BrowserRouter>
props
として渡した router
のtransitionTo()
を使用することで画面遷移ができます。
1handleForm(ev) {
2 ev.preventDefault();
3 BookAction.create(this.state);
4 this.props.router.transitionTo("/books");
5}
BrowserRouter 直下では router
の他に、 action
location
(後述) が取得できます。
現在の URL を取得する
現在の URL に応じてナビゲーションバーのアクティブを変更することも、以下のように location
から pathname
を取得することで実装できます。
1<BrowserRouter>
2 {({ location }) => (
3 <div>
4 <nav className="navbar navbar-inverse">
5 <div className="container">
6 <ul className="nav navbar-nav">
7 <li className={location.pathname === '/menu1' ? 'active' : ''}>
8 <Link to="/menu">Menu1</Link>
9 </li>
10 <li className={location.pathname === '/menu2' ? 'active' : ''}>
11 <Link to="/menu2">Menu2</Link>
12 </li>
13 <li className={location.pathname === '/menu3' ? 'active' : ''}>
14 <Link to="/menu3">Menu3</Link>
15 </li>
16 </ul>
17 </div>
18 </nav>
19 <div className="container">
20 <Match exactly pattern="/menu1" component={Menu1} />
21 <Match exactly pattern="/menu2" component={Menu2} />
22 <Match exactly pattern="/menu3" component={Menu3} />
23 </div>
24 </div>
25 )}
26</BrowserRouter>
3. Hot Module Replacement への対応
react-router v4
を使って動かすだけなら上までで動きますが、 HMR
に対応させるのにも手間が掛かったので、別項目として書きます。
webpack
のに plugin を使うように指定したり、 babel
の plugin を指定したりが必要になります。
本番ビルド向けの webpack.config に加えて、こんな感じに設定を追加してあります。
以下でそれぞれ必要な手順を簡単に説明します。
webpack.config.dev.js の設定
HRM
用の plugin 等を追記した開発用の webpack
の config は以下の通りです。
1const path = require("path");
2const webpack = require("webpack");
3const HtmlWebpackPlugin = require("html-webpack-plugin");
4
5module.exports = {
6 entry: [
7 "react-hot-loader/patch",
8 `webpack-dev-server/client?http://${devServerHost}:${devServerPort}`,
9 "webpack/hot/only-dev-server",
10 "./src/index",
11 ],
12
13 plugins: [
14 new webpack.HotModuleReplacementPlugin(),
15 new webpack.NamedModulesPlugin(),
16 new HtmlWebpackPlugin({
17 hash: false,
18 template: "./src/index.html",
19 }),
20 new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /nb/),
21 ],
22
23 devtool: "inline-source-map",
24
25 output: {
26 path: path.resolve(__dirname, "public"),
27 publicPath: "/",
28 filename: "bundle.js",
29 },
30
31 module: {
32 loaders: [
33 {
34 test: /\.jsx?$/,
35 exclude: /node_modules/,
36 loader: "babel",
37 },
38 ],
39 },
40
41 resolve: {
42 extensions: ["", ".js", ".jsx"],
43 },
44};
.babelrc への plugin 指定
react-hot-loader/babel
plugin が必要なので、 .babelrc
で指定します。
1{
2 "presets": ["es2015", "react"],
3 "plugins": ["react-hot-loader/babel"]
4}
webpack-dev-server の起動
webpack-dev-server を起動させる際は、オプションが記述されている devserver.js
ファイルを用意し、これを起動します。
1const webpack = require("webpack");
2const WebpackDevServer = require("webpack-dev-server");
3
4const config = require("./webpack.config.dev.js");
5
6new WebpackDevServer(webpack(config), {
7 publicPath: config.output.publicPath,
8 contentBase: "src",
9 inline: true,
10 hot: true,
11}).listen(8080, "localhost", (err) => {
12 if (err) return console.log(err);
13});
contentBase
に指定した /src
を起点として開発サーバが起動します。
React Component への設定
これだけだと自動で更新されないので、アプリ全体を AppContainer
以下に置く必要があります。
ここでは entry
である index.js
を以下のようにしています。
1"use strict";
2
3import React from "react";
4import ReactDOM from "react-dom";
5import { AppContainer } from "react-hot-loader";
6
7import App from "./components/app";
8
9ReactDOM.render(<App />, document.querySelector("#app"));
10
11// For Development
12if (module.hot) {
13 module.hot.accept("./components/app", () => {
14 const NextApp = require("./components/app").default;
15 ReactDOM.render(
16 <AppContainer>
17 <NextApp />
18 </AppContainer>,
19 document.querySelector("#app")
20 );
21 });
22}
プロダクションビルドでは自動リロードの機能が動かないようになっていますが、ちゃんとファイルから除きたかったらいろいろ工夫する必要がありそうです。
(イケてる解決方法があれば教えてください……)
これで react-router
でも画面が差分更新されて開発が効率化できます。
4. 最後に
本文中で説明に出したコードを用いた実装は以下にあります。
npm start
を実行すると Hot Module Replacement
が動く開発サーバが起動するので、是非試してみてください。
yutaszk/flux-react-router-v4-hmr-example
明日のReact Advent Calendar 2016 7 日目は @amagitakayosi さんによる React 本体のコード解説 です。
5. 参考