前回、ホットリロードに対応して、React-Bootstrapを使ってナビゲーションバーを実装(見た目だけ)するところまでやりました。
今回は、ナビゲーションバーをクリックすることで画面を切り替えるようにします。
React-Router
ナビゲーションバーの「Link1」「Link2」でそれぞれ画面を切り替えます。URLで表示するReactコンポーネントを切り替えることで対応します。これはReact-Routerを使うと実現できます。
- GitHub - reactjs/react-router: A complete routing solution for React.js
- React初心者のためのreact-routerの使い方 - ハッカーを目指す白Tのブログ
$ npm install history react-router --save-dev
ReactDOM.render
の第1引数をReact-Routerのコンポーネントに書き換えます。
追記 2016/2/21
React Router 2.x系からhistoryの部分の書き方が変わったようです。
react-router/Histories.md at latest · reactjs/react-router · GitHub
以下のコードの var createBrowserHistory = require('history/lib/createBrowserHistory');
ではなく var browserHistory = require('react-router').browserHistory
とします。
追記ここまで。
// react-app/src/index.js var Router = require('react-router').Router; var Route = require('react-router').Route; var IndexRedirect = require('react-router').IndexRedirect; var createBrowserHistory = require('history/lib/createBrowserHistory'); ReactDOM.render( <Router history={createBrowserHistory()}> <Route name="TOP" path="/" component={TopLevel}> <IndexRedirect from="*" to="link1" /> <Route path="link1" component={Page1} /> <Route path="link2" component={Page2} /> </Route> </Router>, document.getElementById('root') );
これで以下のようにルーティングされます。
/
にアクセス- =>
/link1
にリダイレクト
- =>
/link1
にアクセス/link2
にアクセス
TopLevelコンポーネントで子要素を表示するように変更します。{this.props.children}
を使います。
// react-app/src/components/TopLevel.js module.exports = React.createClass({ render: function() { return ( <div> <TopLevelNavBar /> {this.props.children} <footer>footer</footer> </div> ); } });
適当にPage1コンポーネントを用意します。
// react-app/src/components/Page1.js var React = require('react'); module.exports = React.createClass({ render: function() { return ( <div> Page1. </div> ); } });
同じようにPage2コンポーネントも用意します。
server.jsにあるwebpack-dev-serverの設定にhistoryApiFallback: true
を追加。
// react-app/server.js new WebpackDevServer(webpack(config), { ・・・ historyApiFallback: true }).listen(3000, 'localhost', function (err, result) { ・・・
webpack-dev-serverを再起動してアクセス。ナビゲーションバーのリンクをクリックすると表示されるコンポーネントが切り替わります!
しかし、画面のリロードが発生してしまってます!これは望んでた動きじゃないです。リロードなしで表示されるコンポーネントを切り替えたい!!
これは、リンク要素をReact RouterのLinkコンポーネントで実装することで実現できます。しかし、ナビゲーションバーのリンク要素はReact-BootstrapのNavItemコンポーネントで実装しているため、Linkコンポーネントは使えません。こういう時に使うのがreact-router-bootstrap。
$ npm install react-router-bootstrap --save-dev
react-router-bootstrapのLinkContainerコンポーネントでNavItem コンポーネントを囲みます。
// react-app/src/components/TopLevel.js var LinkContainer = require('react-router-bootstrap').LinkContainer; ・・・ <Nav> <LinkContainer to={"/link1"}><NavItem>Link1</NavItem></LinkContainer> <LinkContainer to={"/link2"}><NavItem>Link2</NavItem></LinkContainer> </Nav>
これでブラウザのリロードなしでPage1コンポーネントとPage2コンポーネントの切り替えができるようになりました!
Dynamic Children と key
メニューのリンクの数はルーティングの設定から動的に取得できると嬉しいですね。this.props.route
を使って取得できます。
この時Routeコンポーネントのpropsを使うと便利です。Routeコンポーネントにname
というpropを追加します。
// react-app/src/index.js <Route name="TOP" path="/" component={TopLevel}> <IndexRedirect from="*" to="link1" /> <Route path="link1" name="page1" component={Page1} /> <Route path="link2" name="page2" component={Page2} /> </Route>
TopLevelコンポーネントで子供のRouteコンポーネントの一覧をthis.propts.route
を使って取得します。
// react-app/src/components/TopLevel.js var TopLevel = React.createClass({ ・・・ <TopLevelNavbar route={this.props.route} /> ・・・ }); var TopLevelNavbar = React.createClass({ ・・・ <Nav> {this.props.route.childRoutes.map(function(item) { return ( <LinkContainer to={"/"+item.path}><NavItem>{item.name}</NavItem></LinkContainer> ) })} </Nav> ・・・ });
これでナビゲーションバーのリンクが増えた場合でも、Routeコンポーネントを増やすだけで対応できるようになりました。
しかし、コンソールを見てみると以下のような警告メッセージが出力されてます。
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `TopLevelNavBar`. See https://fb.me/react-warning-keys for more information.
動的にコンポーネントのリストを作るような時、key
というpropにユニークな値をつけておいた方が良いようです。こちらが参考になります。 React.jsの地味だけど重要なkeyについて - Qiita
ユニークなkey
を設定しましょう。item.path
ならユニークになります。これで警告メッセージも消えます。
// react-app/src/components/TopLevel.js {this.props.route.childRoutes.map(function(item) { return ( <LinkContainer key={item.path} to={"/"+item.path}><NavItem>{item.name}</NavItem></LinkContainer> ) })}
データ一覧表示
次はサーバーからデータを取得して表示します。
grails-react-boilerplateでは、Grailsでサーバーアプリケーションが作られています。起動するには以下のようにします。
$ ./grailsw grails> run-app
ではまずは、BookIndexPageコンポーネントをつくって、それをルーティングに登録してみましょう。
// react-app/src/components/BookIndexPage.js var React = require('react'); module.exports = React.createClass({ render: function() { return ( <div> <h1>BookIndexPage</h1> </div> ); } });
// react-app/src/index.js var BookIndexPage = require('./components/BookIndexPage'); ReactDOM.render( <Router history={createBrowserHistory()}> <Route name="TOP" path="/" component={TopLevel}> <IndexRedirect from="*" to="link1" /> <Route path="book" name="book-index" component={BookIndexPage} /> <Route path="link1" name="page1" component={Page1} /> <Route path="link2" name="page2" component={Page2} /> </Route> </Router>, document.getElementById('root') );
BookIndexPageコンポーネントができました。この中でBookListコンポーネントをつくってそこにサーバーから読み込んだデータを表示したいと思います。
react-bootstrap-tableを使ってテーブル要素をつくります。
$ npm install react-bootstrap-table --save-dev
データはstateで管理します。ひとまず、固定のデータで。
// react-app/src/components/BookIndexPage.js var BootstrapTable = require('react-bootstrap-table').BootstrapTable; var TableHeaderColumn = require('react-bootstrap-table').TableHeaderColumn; require('react-bootstrap-table/css/react-bootstrap-table-all.min.css'); module.exports = React.createClass({ getInitialState: function() { var data = [ {"id": 1, "title": "hoge", "price": 200}, {"id": 2, "title": "fuga", "price": 500} ]; return {booklist: data}; }, render: function() { return ( <div> <h1>Books</h1> <BootstrapTable data={this.state.booklist} hover condensed pagination deleteRow selectRow={{ mode: 'checkbox', bgColor: "rgb(238, 193, 213)", }} > <TableHeaderColumn dataField="id" dataSort={true} isKey={true} >ID</TableHeaderColumn> <TableHeaderColumn dataField="title" dataSort={true}>Title</TableHeaderColumn> <TableHeaderColumn dataField="price" dataSort={true}>Price</TableHeaderColumn> </BootstrapTable> </div> ); } });
こんな感じになります。
では、サーバーから取得したデータを一覧表示する様にしましょう。サーバーとの通信処理は、ajax.jsという別ファイルに書きます(このへんの設計をFluxにするかどうかを検討するのかな?)。
// react-app/src/ajax.js var $ = require('jquery'); var urlBase = '/api/'; exports.getBooks = function (callback, callbackError) { $.ajax({ type: 'GET', url: urlBase + 'books.json?max=100', contentType: 'application/json', dataType: 'json', cache: false, success: function(data) { callback(data) }, error: function(xhr, status, err) { console.error(xhr, status, err.toString()) if (callbackError !== undefined) { callbackError(err) } } }); }
公式サイトのチュートリアルにも出てきたように、BookIndexPageコンポーネントに componentDidMount メソッドを追加して、サーバーからデータを取得します。
// react-app/src/components/BookIndexPage.js var ajax = require('../ajax'); module.exports = React.createClass({ getInitialState: function() { return {booklist: []}; }, componentDidMount: function() { var that = this; ajax.getBooks(function(data) { that.setState({ booklist: data }); }); }, ・・・
server.jsにproxyの設定を追加して、ローカルで動いているGrailsアプリケーションのAPIを呼ぶようにします。
// react-app/server.js new WebpackDevServer(webpack(config), { ・・・ proxy: { '/api/*': "http://localhost:8080", }, }).listen(3000, 'localhost', function (err, result) {
server.jsを再起動して、アクセスするとサーバーから取得したデータが表示されます!
データ登録
データの登録フローをつくります。下の流れのようにします。
- Newボタンをクリック -> ダイアログが表示される
- ダイアログ内のフォームに入力して投稿 -> サーバーにデータが登録され、一覧に入力したデータが表示される
ボタンはReact-BootstrapのButtonコンポーネント、ダイアログはReact-BootstrapのModalコンポーネントを使います。では、BookNewDialogコンポーネントをつくりましょう。
// react-app/src/components/BookNewDialog.js var React = require('react'); var Modal = require('react-bootstrap').Modal; var Input = require('react-bootstrap').Input; var Button = require('react-bootstrap').Button; module.exports = React.createClass({ callbackSubmitButtonAction: function() { this.props.submitButtonAction({ title: this.refs.title.getValue(), price: this.refs.price.getValue() }); }, getInitialState: function() { return {book: null}; }, render: function() { return ( <Modal show={this.props.show} onHide={this.props.closeAction}> <Modal.Header closeButton> <Modal.Title>New Book</Modal.Title> </Modal.Header> <Modal.Body> <form ref='form' className="form-horizontal"> <Input ref="title" type="text" label="Title:" labelClassName="key col-xs-2" wrapperClassName="col-xs-10" defaultValue={this.state.book && this.state.book.title} /> <Input ref="price" type="text" label="Price:" labelClassName="key col-xs-2" wrapperClassName="col-xs-10" defaultValue={this.state.book && this.state.book.price} /> </form> </Modal.Body> <Modal.Footer> <Button bsStyle="primary" onClick={this.props.closeAction}>Close</Button> <Button bsStyle="success" onClick={this.callbackSubmitButtonAction}>Create</Button> </Modal.Footer> </Modal> ); } });
親コンポーネントから受け取ったsubmitButtonActionやcloseActionをonClick時のアクションとして指定します。フォームの値はstateを使って管理します。
BookListコンポーネントのJSXを変更して、BookNewDialogコンポーネントを追加します。
// react-app/src/components/BookNewDialog.js <Button onClick={this.showNewDialog}>New</Button> <BootstrapTable data={this.state.booklist} ・・・ </BootstrapTable> <BookNewDialog show={this.state.showNewDialog} closeAction={this.hideNewDialog} submitButtonAction={this.createBook} />
使用してるメソッドをつくっていきます。
// react-app/src/components/BookIndexPage.js module.exports = React.createClass({ createBook: function(creatingBook) { this.setState({showNewDialog: false}); console.log(creatingBook); }, showNewDialog: function() { this.setState({showNewDialog: true}); }, hideNewDialog: function() { this.setState({showNewDialog: false}); }, ・・・
ここまででダイアログを表示して、入力された値をcreateBookメソッドで受け取るところまでできました(ダイアログを表示するかどうかのstateを変えるのってFlux的にやるならどうなるんでしょうね)。
あとは、createBook
メソッドでサーバーにPOSTすればいいですね。ajax.jsにメソッドを追加します。
// react-app/src/ajax.js exports.createBook = function (book, callback, callbackError) { $.ajax({ type: 'POST', url: urlBase + `books.json`, contentType: 'application/json', dataType: 'json', data: JSON.stringify(book), cache: false, success: (data) => { if (callback !== undefined) { callback(data) } }, error: (xhr, status, err) => { console.error(xhr, status, err.toString()) if (callbackError !== undefined) { callbackError(err) } } }); };
BookListコンポーネントから、ajaxモジュールのメソッドを呼び出すように変更します。thisが・・・。
// react-app/src/component/BookList.js module.exports = React.createClass({ createBook: function(creatingBook) { this.setState({showNewDialog: false}); var that = this; ajax.createBook(creatingBook, function(){ that.reloadData(); }, function(err) { console.log("error"); }); }, reloadData: function() { var that = this; ajax.getBooks(function(data) { that.setState({ booklist: data }); }); }, ・・・
これでデータをサーバーに登録できて、登録したデータがすぐに一覧に追加されます。
おしまい
今回は、React-Router、Dynamic Children と key、サーバーとのHTTP通信をやりました。
grails-react-boilerplateではBabelを使ってES2015でJavaScriptを書けるように対応しています。次回、Babelを使うようにするのとESLintを入れてみます。