React + Redux に取り組んだ際のハマりポイント振り返り
javascript 初学者が React + Redux に取り組んだ際のハマりポイント、時間がかかったポイントを時系列で書いていきます。
振り返りのまとめ
- React も Redux もドキュメント充実してるのでちゃんと読むべき
- フロントエンド開発環境は空気読むのが難しい
動機
- javascript ぜんぜん書いてないので、よさそうな ES2015 とやらが出たこの機会に触っておきたかった
- Single Page Application を作る機会があった
- 一人でさっと作る類のものなので、せっかくなので近年話題のものを調べてみたい
振り返り
※ 最終的にできあがったサンプルはこちらに置いています。
ハマり Points
React について
そもそも正確には何をするライブラリで、どの範囲を指している言葉なのか? これはハマりというか事前の理解が違っていました。ぼんやりと複雑なものを想像していましたが、概念的にはむしろシンプルで小さいというのが今の理解です。
https://facebook.github.io/react/docs/getting-started.html
公式のドキュメントがしっかりしています。StarterKit によると
- Just the UI
- Virtual DOM
- Data flow
の UI ライブラリだそうです。この時点ではちょっとピンと来ていなかったですが、フレームワークの類ではなく本当に View でしかないものです。
もう少し React
雑に言えば、Component というのをたくさん作ってそれを組み合わせ仮想 DOM を構築し、それをよい具合にリアル DOM に変換してくれるライブラリ…でよい?
Component
Component とはパラメータを渡せる UI 部品で
- JSX という HTML 風シンタックスを使って書ける
- ライフサイクルイベントを持っている。
- 最小限にすべきだが、Component ごとの state を持つこともできる
というものです。
Component の組み合わせ方としては、通常の HTML 要素を組むように Component のツリー構造を作り、最上位の ReactDOM.render()
に渡せばよいようです。公式チュートリアル が良かったです。
このチュートリアルは cdn からライブラリを取得し index.html
と example.js
にモリモリ書いていくだけなのでハマりどころが無くわかりやすかったです。一点 <script type="text/babel">
ってなんだ…?と思いつつも、簡単に動くものが作れます。
フロントエンドの開発環境
チュートリアルまで終えたところで、じゃあ npm 使ってパッケージ管理して書いていこうと思ったわけですが、この辺で javascript というかフロントエンド開発環境の複雑さに気づきました…。
まだ過渡期なのかもしれませんがちょっとバラバラしすぎている印象が…。調べながら現代に追いつくのが困難でした。ググッて 2014 くらいだと既に変わっていることが多いので、2015後半〜 くらいの期間指定検索で追いかけていました。最近は「一新した」系記事が多いところを見るとそろそろ落ち着いたのかな…?
babel
動機の一つであった ES2015 はブラウザ処理系ではほぼ使えないので、トランスパイラをはさむのが普通らしく、これは babel というのを使っておけばよさそうです。しかし、どうビルドしていくかについては、facebook の StarterKit の段階では grunt なるものを使うガイドになっていましたが、glup がいいよとか npm の script
で十分行けるとか、webpack がよいとか調べるほど混乱が増えてさっそく一度折れかけました。
結局、今の時点でツールを増やしたくないので npm script でやります。http://mae.chab.in/archives/2765 を参考にし、watchify + babelify で ruby の Guard っぽいことができるということでこれを使ってみます。
npm install --save-dev
を知りました。
$ npm install --save-dev babel-plugin-transform-object-rest-spread $ npm install --save-dev babel-preset-es2015 babel-preset-react browserify watchify babelify
.babelrc
{ "presets": ["react", "es2015"], "plugins": ["transform-object-rest-spread"] }
package.json
"scripts": { "watch": "watchify -v -t babelify -o public/bundle.js src/index.jsx" }
監視開始
$ npm run watch
src/index.jsx
を編集するそばから即反映されるようになりました。欲しかった import
も、 () => {}
も、 const/let
も、{...obj}
も使えます。よさそうです。
ファイル拡張子については、JSX が含まれるものは .jsx
, そうでないものは .js
としてみます。(全部 .js
のパターンのほうが多そうで迷ってます。どうなんでしょう)
スタイルガイド
書き始めると、自分では良いスタイルが全然わからないので、Rubocop のようなものがほしいところです。
普段は Emacs で書きながら FlyCheck でチェックさせているので、近いものをさがしていたらまさにFlyCheck で ESLint するという記事を書かれている方がいましたので、参考にさせていただきました。
$ npm install -g eslint
.eslintrc
{ "parser": "babel-eslint", "env": { "es6": true }, "ecmaFeatures": { "jsx": true } }
本来は eslint も package.json
管理下とするのがよさそうですが、現在の自分のエディタ設定の都合でグローバルに入れちゃいます。そのうちなんとかしたい。
JSX のシンタックスって?
単一の要素しか返せないのか? パラメータをまとめて渡すにはどうすれば?
などで無駄につまづいてしまいました。これは最初に https://facebook.github.io/react/docs/jsx-in-depth.html を読むべきでした。React.createElement()
の別の記法だと理解していれば非常にシンプルです。
var Nav, Profile; // Input (JSX): var app = <Nav color="blue"><Profile>click</Profile></Nav>; // Output (JS): var app = React.createElement( Nav, {color:"blue"}, React.createElement(Profile, null, "click") );
パラメータについても、1つずつ <Comp param={value} />
でも良いしまとめて <Comp {...props} />
でも渡せます。
Debug どうすれば
トランスパイラをかませるとデバッガ上の表示が元ソースと別になる問題でしたが、source map という仕組みがありました。近年のブラウザは問題なく対応してるようで割と何年も前からあるみたいです。常識でしたか…
watchify に -d
オプションと exorcist
挟むのを追加。
$ npm install --save-dev exorcist
package.json
"scripts": { "watch": "watchify -v -t babelify -o \"exorcist ./public/bundle.js.map > public/bundle.js\" src/index.jsx -d", "build": "watchify -v -t babelify -o public/bundle.js src/index.jsx" }
エラー行の対応付け、ブレークポイントの設定ができるようになりました。しかし生成されるファイルは非常に巨大になるので dev 専用です。build と分けました。
Redux って?
Reducer, actionCreator, connect, mapXXXToProps, ... 見慣れない言葉が多くて全体が見えず、二度目の折れかけポイントでしたが、資料は少なくないので路頭に迷わずにはすみました。
最初に http://redux.js.org/index.html の 2.Basics
まで目を通します。その後、こちらのビデオ をざーっと見て概要を理解できた気になれました。
概要はわかったものの、動く状態が無いとよくわからないので無理やり動くものを用意します。ファイル分割は一旦おいといて、index.jsx にまとめて書いてみました。
$ npm install --save react react-dom redux react-redux
index.jsx
import React, { Component } from 'react' import ReactDOM from 'react-dom' import { createStore } from 'redux' import { connect, Provider } from 'react-redux' /// actionCreators >>> function action1(value) { return { type: 'ACTION1', value } } /// <<< /// reducers >>> function appReducer(state, action) { switch (action.type) { case 'ACTION1': return Object.assign({}, state, { v1: action.value }); default: return state } } /// <<< /// components >>> const Comp1 = (props) => ( <div>{console.log(props)} <div>This is Comp1. value is '{props.value}'.</div> <button onClick={() => props.action('VALUE')} >doAction1</button> </div> ); /// <<< class RootApp extends Component { render() { return ( <div> Hello, this is root component. <Comp1 value={this.props.value1} action={this.props.action1} /> </div> ) } } const App = connect( (state) => { return { value1: state.v1} }, // mapStateToProps (dispatch) => { return { action1: (v1) => dispatch(action1(v1)) } }// mapDispatchToProps )(RootApp); const rootElement = document.getElementById('content'); const initialValues = { v1: 'initialvalue' }; const store = createStore(appReducer, initialValues); ReactDOM.render( <Provider store={store}> <App /> </Provider>, rootElement );
これで最低限は動くはずです。意図的に変な名前の部分と便利なヘルパーを使ってない部分がありますが、最終的には整理しています。
確認を兼ねてちょっと説明
先ほどの例で上のほうにあるのが Redux 世界のもので、
- actionCreator は action を返す関数。
- action というのはただのオブジェクト。
type
が必須。 - reducer は、
dispatch(action)
された時に呼ばれる関数。全体の状態ツリーである state と action が渡ってくるので、action に応じて新しい state オブジェクトを生成して返す。 createStore()
で全体の状態を保持する store を作成する。これには reducer と、必要なら初期値を与える。 - reducer は、state, action を受け取って新しい state を返す関数。
ここまでが Redux の枠組みかと思います。UI に直接は関係しないので、この時点で store.dispatch(action)
のようにテストできます。(とビデオでもやってました)
次に、Redux 世界と React との接続については
- おまじない的に store を与えた
<Provider>
をルートに置き、実質のルートになる<App>
コンポーネントを入れる。 -<Provider>
には1つしかコンポーネントを入れられない。 <App>
は、connect()
されたコンポーネントである必要がある - このconnect()
というのは、Redux の store を React の props に接続するもの - 第一引数が、reducer で返された state を props にマップする関数 - 第二引数が、actionCreators を props にマップする関数
となります。Component に props が渡った先は React の文脈になるので、普通に細かく Component を書いていけば OK です。ひとつ違うのは、Redux では state は1つなので、Component 自体の state を変更はできず、動的な値を扱うには上からパラメータを渡してもらうしかありません。
Redux のデバッグ
redux-logger ミドルウェアを入れると、dispatch の度に state を表示してくれます。
$ npm install --save redux-logger
redux-devtools なる本格的なのもあるようですがこちらはまだ試していません。
Redux で非同期 dispatch
どこで非同期リクエストのコードを書けば良いのかについて、http://redux.js.org/index.html の 3. Advanced 以降に詳しく書いてあります。このあたり。 redux-thunk ミドルウェア を使って actionCreator で fetch すればよいです。action を細かく分けると混乱せずに済みます。
$ npm install --save redux-thunk
非同期 API
前後しますが、そもそも非同期リクエストって何使えばよいの…って時点でもつまづいていました。
React Tutorial では、わかりやすさを重視したのか jQuery の $.ajax
を使っていましたが、リクエスト投げたいだけなのに jQuery を読み込むのは間違っている気がしますし、単独の非同期リクエスト用ライブラリで何がデファクト標準なのかを探すのに時間を取られてしまいました。
GitHub star や雰囲気を見ながらですと superagent や axios もよさそうですが、複雑なことはしないので単純でよいというのもあり、redux-async の例で使われていた fetch(isomorphic-fetch) でやっています。
$ npm install --save isomorphic-fetch
Redux で form
React の form の例を探すと、該当 Component に input 値の state を持たせるパターンばかりが見つかります…。
Redux で使えないため、値を伝搬させるのを自力で書こうかと思いましたが明らかに面倒くさいので redux-form ミドルウェア を使います。
$ npm install --save redux-form
非同期の redux-thunk のときもそうでしたが、こういう基本的な機能は本体付属のライブラリ無いのかな?と思いましたが無いようです。何気なくつぶやいたら Twitter で教えて頂けてとても安心しました。雰囲気としてどんどん外部ライブラリ使っていく、というものなんでしょう。
Redux のディレクトリ、ファイル構成
多くの例にあるとおりにしています。
src ├── actions # redux actionCreators ├── reducers # redux reducer functions ├── store # redux middleware 使えるようにするところ ├── containers # react-redux 接続部分 ├── components # UI 部品 └── index.jsx # エントリポイント
特に containers と components について、公式ドキュメントの http://redux.js.org/docs/basics/UsageWithReact.html が重要そうです。シンプルな場合なら、components には Redux 世界のものは全く含まれない状態になると思います。
redux-form の接続部分は containers なのか component なのかまだ理解が浅いです…。
Components の書き方
一つ一つの Component は割とスカスカになりそうに見えます。ライフサイクルイベント(componentDidMount
とか) がどうしても必要な場合を除き、関数スタイルで書くのがすっきり書けてよさそうです。
import React, { PropTypes } from 'react' const MyItem = (props) => { return ( <span>{props.name} : {props.value}</span> ) } MyItem.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.string.isRequired } export default MyItem
最終的に
こうなりました(再掲) https://github.com/komazarari/react-redux-form-example
すっきりしましたが、特に reducer や form にまだ改善の余地がありそうです。
足りないこと
今回は CSS について触れていません。webpack
とやらを使えばすっきりするらしいので引き続き試してみたいと思います。