読者です 読者をやめる 読者になる 読者になる

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.htmlexample.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.html2.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 や雰囲気を見ながらですと superagentaxios もよさそうですが、複雑なことはしないので単純でよいというのもあり、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 とやらを使えばすっきりするらしいので引き続き試してみたいと思います。