DropboxのMarkdownをちょっと便利に使うツールをつくった
grails-react-boilerplate で React に入門した - 3日目 Babel, ESLint
前回、React Routerを使ったルーティング、サーバーとのデータ通信までできました。しかし、これまでECMAScript 5 (ES5) でJavaScriptを書いてきましたが、grails-react-boilerplateではECMAScript 2015 (ES2015) でJavaScriptが書かれています。
今回はBabelを使って、ES2015に対応できるようにしてみます。
引き続き、以下の記事とリポジトリを参考に進めます。
Babel
以前は6to5と呼ばれていたトランスパイラ。JSXも標準でサポート。本稿ではwebpackのローダの一つとして使用
Babelの説明はこう書かれてます。トランスパイラとはコード変換ツールのようなものっていう理解でいいのでしょうか。Babelを使うとES2015のコードをES5に変換できます。ES2015だけでなく、JSXも変換してくれます。
Babelはプラグインを組み合わせて動作させるようです。そして、複数のプラグインをセットにしたものをpresetと呼ぶようです。
今回使いたいpresetは、es2015とreactです。
WebpackからBabelを使えるようにしましょう。Webpackのloaderとして、babel-loaderが使えます。
GitHub - babel/babel-loader: Webpack plugin for Babel
Babelのオプションで使用するpresetを選べます。webpack.config.jsにloader: 'babel?presets[]=react,presets[]=es2015'
というように書いてもいいですが、.babelrcというファイルに書くこともできます。.babelrcを作成しましょう。
// .babelrc { "presets": ["es2015", "react"] }
npmでモジュールをインストールします。
$ npm install babel-core babel-preset-es2015 babel-preset-react babel-loader --save-dev
webpack.config.jsでloaderの設定をjsx
からbabel
へ変更します。
// react-app/hot.webpack.config.js loaders: ['react-hot', 'babel'],
これで設定ができました!
ES2015の文法はWebで調べるとして、ReactでES2015を使う場合、React.createClass
メソッドを使わずにclassとしてReactコンポーネントを定義できます。
BookIndexPageコンポーネントをES2015で書き換えてみます。
// react-app/src/components/BookIndexPage.js import React, {Component} from 'react'; import {BootstrapTable, TableHeaderColumn} from 'react-bootstrap-table'; import {Button} from 'react-bootstrap'; import 'react-bootstrap-table/css/react-bootstrap-table-all.min.css'; import * as ajax from '../ajax'; import BookNewDialog from './BookNewDialog'; export default class BookIndexPage extends Component { constructor(props) { super(props); this.state = {booklist: []}; } createBook(creatingBook) { this.setState({showNewDialog: false}); ajax.createBook(creatingBook, (_) => { this.reloadData(); }, (err) => { console.log("error"); }); } reloadData() { ajax.getBooks((data) => { this.setState({ booklist: data }); }); } showNewDialog() { this.setState({showNewDialog: true}); } hideNewDialog() { this.setState({showNewDialog: false}); } componentDidMount() { ajax.getBooks((data) => { this.setState({ booklist: data }); }); } render() { return ( <div> <h1>Books</h1> <Button onClick={this.showNewDialog.bind(this)}>New</Button> <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> <BookNewDialog show={this.state.showNewDialog} closeAction={this.hideNewDialog.bind(this)} submitButtonAction={this.createBook.bind(this)} /> </div> ); } }
webpack-dev-serverを起動してアクセスすると、無事表示されます。
grails-react-boilerplateでは、stage-0
というpresetも使っています。これは何でしょうか?
調べてみると、ECMAScriptでは新機能が仕様として認められるかどうかの確度を5段階のStageで表記するようです。
Stage0の段階の機能も使いたい場合は、Stage0のpresetも使えるように設定しておく必要があります。
$ npm install babel-preset-stage-0 --save-dev
// react-app/.babelrc { "presets": ["es2015", "stage-0", "react"] }
ESLint
ESLintはJavaScriptの静的コード解析ツール。早速インストールしてみます。
$ npm install eslint --save-dev
package.jsonにeslintを実行するscriptを追加。
// react-app/package.js "scripts": { ・・・ "lint": "eslint src" },
これで実行できます。
$ npm run-script lint
たくさんエラーが出ました!ES2015で記述してるので、ESLintにES2015を使ってることを教えてあげないといけないです。ESLintの設定ファイルは .eslintrc です。
ecmaFeatures
でES2015の文法を個別に有効にすることができるようです。env
で"es6": true
を設定するとmodules以外が有効になるようです(ECMAScript 2015は以前はECMAScript 6という名称で呼ばれてました)。modulesについては個別にecmaFeatures
で"modules": true
を追加します。JSXも使っているので"jsx": true
も追加。
追記 2016/2/21
ESLintのバージョンが2.xになってから ecmaFeatures
の記述を paserOptions
に書くようになったみたいです。
"parserOptions": { "ecmaFeatures": { "jsx": true, "modules": true } },
追記ここまで。
Documentation - ESLint - Pluggable JavaScript linter
// react-app/.eslintrc { "env": { "es6": true }, "ecmaFeatures": { "jsx": true, "modules": true }, "rules": { } }
これでnpm run-script lint
するとエラーなく実行されます。
.eslintrcにルールを追加していくと色々チェックできるようになります。例えば、セミコロンの省略をチェックするようにするには"semi": 2
を追加します。2
という数値を指定するとチェックに引っかかった場合にエラーとします。1
を指定すると警告です。
List of available rules - ESLint - Pluggable JavaScript linter
// react-app/.eslintrc "rules": { "semi": 2 }
おしまい
今回は、BabelとESLintを導入してみました。ここまでくれば後はモリモリと写経できそうです!
grails-react-boilerplate で React に入門した - 2日目 React-Router, サーバー通信
前回、ホットリロードに対応して、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を入れてみます。