React.js の仕事 ほぼ完了しました!

ここ数ヶ月、久々に詰めて行っていた Recat.js の仕事がほぼ終わりました !!

f:id:yuum3:20160204101138p:plain

仕事の詳細は書けませんが。 既存のjQueryベースのWebアプリがメンテナンス不可能に近くなっているので Recat.js に置き換えました。

既存のアプリは

  • Backend は Ruby on Rails
  • jQueryで作られてた Frontend の JS は約 14K 行 + テンプレート 4K 行
  • アプリは大きく3つに別れるが、共有部分無しでコピーペーストで作られている

今回の仕事

  • Backend は 落ちていたテストを修正したくらい
  • End to End テストが無かったので書いた
    • Turnip(RSpec, Gherkin) + Capybara + Poltergeist + Phantom.js
    • Featureファイル 2K行、 steps 1.2K行
    • 開発期間 約1ヶ月
  • jQuery から Reactへの移行

感想など

  • 既存アプリは構成など初期設計は良く出来ている感じで実はそれほど悪いものではないが、実装はコピーぺの山だし無意味な部分も多数あった
  • jQueryベースのものはページの変更後の再表示がかなりの部分で全書き換えになっていたが、 React版は Reactのおかげで最小限の書き換えになり高速化された!
  • End to End テストを書くことでFrontendの動き、実装に関する理解が深まりました
  • React.js は評判通りコードが長くなる、しかし構造が判りやすくメンテナンスしやすいのではと思える
  • アプリの行っている事が比較的シンプルで Flux/Redux 等は コード量が増える割に、メリットが少なそうだと思えたので採用しなかった
  • さすがに1万行くらいになるとWebpack(ESLint+Babel)に時間がかかるように成るがなんとか使えるレベルではあった
  • Reactのコードは長くなるが難しいコードを書いているわけではないので CoC(convention over configuration)なライブラリー/トランスレータが出たら良いな・・・
  • 今回得られた知見を元に React.js の教育も開始したいなと思います。そのために React.jsの入門記事を書きました。
  • End to End テストのおかげで幾つかのバグが発見できましたが、それ以上に修正した後でテストが通れば「まあ酷いバグはないさ〜!」という安心感が良いですね ^^)

サーバーサイドプログラマーのためのReact.js 入門 4. react-router、更新系ページの追加

React.jsの仕事の納期におわれ1ヶ月ぶりになってしましたましたが、今回は

  • react-router の導入
  • 更新系ページの追加

を行い、Todoアプリを完成したいと思います。

f:id:yuum3:20160204101138p:plain

react-router

JSで作られたアプリはSPA(Single Page Application)と呼ばれているように物理的には1つのページ(URL)内で動作します。しかしアプリがURLに対応してないとブラウザーのbackボタンに対応出来ないないですし、プログラムの構成を考えるのも面倒です。

そこで、URLとアプリ(JS)を対応づける react-router を使うと Rails の routes のように URLにアプリを対応づけ出来、今ままで Rails 等で開発してきた人にはとても嬉しいことです。

react-router には チュートリアル や充実したドキュメント がありますので、しっかり理解するには チュートリアル を試し、ドキュメント に目を通しておくと良いと思います。

react-router を組み込んでみる

前回作った TODO 一覧に react-router を組み込んで Railsのshow ページと同じものを追加してみましょう。

f:id:yuum3:20160331151644p:plain f:id:yuum3:20160331151654p:plain

react-router インストール

$ cd frontend
$ npm install --save react-router

index.js ページの作成

前回作ったアプリは全てのコードが src/js/index.js にありましたが、 一覧の機能は src/js/IndexPage.js にしましょう。 src/js/index.js をコピーまたはリネームし src/js/IndexPage.js を作ります(修正は次で行います)

src/js/index.js はルーテイング情報を書きます。

import React from 'react'
import { render } from 'react-dom'
import { Router, Route, IndexRoute, hashHistory } from 'react-router'

import IndexPage from './IndexPage'
import ShowPage  from './ShowPage'

render((
  <Router history={hashHistory}>
    <Route path="/" >
      <IndexRoute component={IndexPage}/>
      <Route path="/:id" component={ShowPage}/>
    </Route>
  </Router>
  ),
  document.getElementById("example")
)

ページ繊維の履歴管理にはいくつかの方法が選べます。 ドキュメントのHistories に説明があります。 今回はhashHistoryを使ってみます、ドキュメントに本来は browserHistory を使うべきだと書かれていますが browserHistory を使うと Rails で使っているURLがかぶってしまうので、ここでは /#URL を使うhashHistoryにしました Router history= で hashHistory を指定しています。

ルーテイング は / で一覧(IndexPage.js) /ID番号 で詳細表示(ShowPage.js) が動作するように指定しています。 Rails のルーテイング同様に /:id の id の部分はパラメーターとして取得できます。

画面へReact.jsの結果を表示する render() はこちらに移動します。

IndexPage.js 変更

前回の index.js をコピーした IndexPage.js は少し変更します。変更点はコメントに ★ を付けたところで

  1. import の変更、 import ReactDOM from 'react-dom' は削除されました
  2. IndexPageクラスが外部から参照できるように export default を追加
  3. Showpage.js へのリンク <Link><a> タグが作成されます、Railslink_to と同じようなものですね。<Link> は単純に <a> を生成しているのでは無く色々な処理を行っています
  4. 画面へReact.jsの結果を表示する render() は index.js に移行したので削除されました
import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'  // ★
import axios from 'axios'

export default class IndexPage extends Component {  // ★
  constructor(props) {
    super(props)
    this.state = {todos: []}
  }
  componentDidMount() {
    axios.get('/todos.json').then((response) => {
      this.setState({todos: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  render() {
    return(
      <div>
        <h2>List of Todos</h2>
        <TodoList todos={this.state.todos}/>
      </div>
    )
  }
}

class TodoList extends Component {
  render() {
    return(
      <table>
        <thead>
          <tr>
            <th>Due</th>
            <th>Task</th>
            <th colSpan="3"></th>
          </tr>
        </thead>
        <tbody>
          {this.props.todos.map((todo) => (<TodoListItem key={todo.id} todo={todo} />))}
        </tbody>
      </table>
    )
  }
}
TodoList.propTypes = {
  todos: PropTypes.array.isRequired
}

class TodoListItem extends Component {
  render() {
    return(
      <tr>
        <td> {this.props.todo.due} </td>
        <td> {this.props.todo.task} </td>
        <td><Link to={`/${this.props.todo.id}`}>Show</Link></td>  // ★
      </tr>
     )
  }
}
TodoListItem.propTypes = {
  todo: PropTypes.object.isRequired
}

ShowPage.js 作成

詳細表示のページ ShowPage.js は以下のようになります。 URLで指定される ID番号は this.props.params.id で取得できます。その他は特に説明する事はないと思います。

import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import axios from 'axios'

export default class ShowPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todo: {}}
  }
  componentDidMount() {
    this.todoFind(this.props.params.id)
  }
  componentWillReceiveProps(nextProps) {
    this.todoFind(nextProps.params.id)
  }
  todoFind(todoId) {
    axios.get(`/todos/${todoId}.json`).then((response) => {
      this.setState({todo: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  render() {
    return(
      <div>
        <h2>Show</h2>
        <p>
          <strong>Due:</strong>
          {this.state.todo.due}
        </p>
        <p>
          <strong>Task:</strong>
          {this.state.todo.task}
        </p>
        <Link to="/">Back</Link>
      </div>
    )
  }
}
ShowPage.propTypes = {
  params: PropTypes.object
}

ここまで出来れば、上の画像のように 一覧の show リンクをクリックすると詳細画面が表示されます

更新系ページの追加

RailsCSRF対策を無効にする

追加・更新を行うにはRuby on RailsCSRF対策に対応しないといけませんが、今回は安易にスキップします。 実際のアプリでは行わないで下さいね!

  • ../app/controllers/todos_controller.rb
class TodosController < ApplicationController
  before_action :set_todo, only: [:show, :edit, :update, :destroy]
  skip_before_action :verify_authenticity_token  # ★

新規作成ページのルーティング情報を追加

まずは、index.js にルーティング情報を追加します、コメントに ★ を付けたところ

import { render } from 'react-dom'
import { Router, Route, IndexRoute, hashHistory } from 'react-router'

import IndexPage from './IndexPage'
import ShowPage  from './ShowPage'
import NewPage   from './NewPage'  // ★

render((
  <Router history={hashHistory}>
    <Route path="/" >
      <IndexRoute component={IndexPage}/>
      <Route path="/new" component={NewPage}/>  // ★
      <Route path="/:id" component={ShowPage}/>
    </Route>
  </Router>
  ),
  document.getElementById("example")
)

新規作成ページの追加

新規作成ページに対応する NewPage、 TODO情報のフォームに対応する TodoForm に分けました。Railsと同じように編集ページと共通になるフォーム部分を別のコンポーネント(モジュール)にしました。

React.js では state(状態、データ)は上位のコンポーネントに置き、下位のコンポーネントへpropsで渡します。 またボタンを押した等のイベントは下位のコンポーネントでは処理せずに上位のコンポーネントで処理するのが定跡(ベストプラクティス)です。

TODO情報をバックエンドに登録(ポスト)する todoCreateメソッド はNewPageに置いています。 todoCreateメソッド は下位のTodoFormの中にも置けますが、登録した後に state が変化する事は良くあるので、この方が良いと思います。また、通信やモデル(ここでは stateのtodo)変更は上位のコンポーネントに集めるのは下位コンポーネントの汎用性やコードの見通しがよくなります。

todoCreateメソッド内の this.props.history.push() は指定されたURL(Path)への遷移を起こします。

formChangeメソッドに付いては TodoForm の方で説明します。

import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import axios from 'axios'
import TodoForm   from './TodoForm'

export default class NewPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todo: {}}
  }
  todoCreate() {
    axios.post('/todos.json',
              {todo: this.state.todo}).then((response) => {
      this.props.history.push(`/${response.data.id}`)
    }).catch((response) => {
      console.log(response)
    })
  }
  formChange(todo) {
    this.setState({todo: todo})
  }
  render() {
    return(
      <div>
        <h2>New</h2>
        <TodoForm todo={this.state.todo} buttonLable="Create" onChange={this.formChange.bind(this)} onSubmit={this.todoCreate.bind(this)} />
        <Link to="/">Back</Link>
      </div>
    )
  }
}
NewPage.propTypes = {
  params: PropTypes.object,
  history: PropTypes.object
}

TodoForm は主にを <form> フォームを扱っています。 React.js でのフォームの扱いは ドキュメントのForms に書かれているます。フォーム内の <input> のハンドリングは、全ての変更をアプリのコードで扱う Controlled Components と Reactに任せる Uncontrolled Components があります。ここでは分かりやすい Controlled Components を使っています。

Controlled Componentsでは<input> に変化がおきるとその変化をアプリのコードで state に反映し、stateの値から <input> を再表示します(再表示は React.js が行います)。 ここでは state は上位コンポーネントにあるので、上位コンポーネントの formChange メソッドを呼び出しています。その結果、上位コンポーネントから変更された state の値を props で受け取った TodoForm が再表示されます(再表示は React.js が行います)。

React.js では <input> の値などを参照するには タグに ref 属性を指定し、コードからは this.refs.ref属性値 で取得します。 refに付いては ドキュメントのRefs to Components に書かれています。

submitボタンが押された時の動作はここでは <button>onClick でハンドリングしています。 event.preventDefault() で他のタグへのイベントの伝搬をキャンセルし、上位コンポーネントメソッドを呼び出しています。

日付の入力はRailsでは年月日の <select> になりますが、面倒なので通常の <input type="text"> にしました。たぶんカレンダー入力の React Date Picker などを使えば良いのかなと思います。

import React, { Component, PropTypes } from 'react'

export default class TodoForm extends Component {
  change() {
    this.props.onChange({due: this.refs.due.value, task: this.refs.task.value})
  }
  submit(event) {
    event.preventDefault()
    this.props.onSubmit()
  }
  render() {
    return(
        <form>
          <div className="field">
            <label htmlFor="todo_due">Due</label><br/>
            <input type="text" ref="due" id="todo_due" value={this.props.todo.due} onChange={this.change.bind(this)} />
          </div>
          <div className="field">
            <label htmlFor="todo_task">Task</label><br/>
            <input type="text" ref="task" id="todo_task" value={this.props.todo.task} onChange={this.change.bind(this)} />
          </div>
          <div className="actions">
            <button onClick={this.submit.bind(this)}>{this.props.buttonLable}</button>
          </div>
        </form>
    )
  }
}
TodoForm.propTypes = {
  todo: PropTypes.object.isRequired,
  buttonLable: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  onSubmit: PropTypes.func.isRequired
}

IndexPage.js の変更1

新規作成へのリンクを追加します

  *** import は変更なし ***
  
export default class IndexPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todos: []}
  }
  componentDidMount() {
    this.todoAll()
  }
  componentWillReceiveProps() {
    this.todoAll()
  }
  todoAll() {
    axios.get('/todos.json').then((response) => {
      this.setState({todos: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  todoDelete(todoId) {
    axios.delete(`/todos/${todoId}.json`).then(() => {
     this.todoAll()
    }).catch((response) => {
     console.log(response)
     })
  }
  render() {
    return(
      <div>
        <h2>List of Todos</h2>
        <TodoList todos={this.state.todos}/>
        <br />
        <Link to="/new">New Todo</Link>  // ★
      </div>
    )
  }
}

     ***  以下は同じ  ***

IndexPage.js の変更2

新規作成機能を追加し試してみると、新規追加したデータが表示されない時があります。前回書いたように

一覧の再表示が正しく行われるように componentWillReceivePropsメソッドでもデータを取得するように変更します。

import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import axios from 'axios'

export default class IndexPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todos: []}
  }
  componentDidMount() {  // ★
    this.todoAll()
  }
  componentWillReceiveProps() {  // ★
    this.todoAll()
  }
  todoAll() {  // ★
    axios.get('/todos.json').then((response) => {
      this.setState({todos: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  todoDelete(todoId) {
    axios.delete(`/todos/${todoId}.json`).then(() => {
     this.todoAll()
    }).catch((response) => {
     console.log(response)
     })
  }
  render() {
     ***  以下は同じ  ***
     

変更ページの追加

新規作成ページと同様なので特に説明する必要はないと思います

  • index.js
import React from 'react'
import { render } from 'react-dom'
import { Router, Route, IndexRoute, hashHistory } from 'react-router'

import IndexPage from './IndexPage'
import ShowPage  from './ShowPage'
import EditPage  from './EditPage'  // ★
import NewPage   from './NewPage'

render((
  <Router history={hashHistory}>
    <Route path="/" >
      <IndexRoute component={IndexPage}/>
      <Route path="/new" component={NewPage}/>
      <Route path="/:id" component={ShowPage}/>
      <Route path="/:id/edit" component={EditPage}/>   // ★
    </Route>
  </Router>
  ),
  document.getElementById("example")
)
  • EditPage.js
import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import axios from 'axios'
import TodoForm   from './TodoForm'

export default class EditPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todo: {}}
  }
  componentDidMount() {
    this.todoFind(this.props.params.id)
  }
  componentWillReceiveProps(nextProps) {
    this.todoFind(nextProps.params.id)
  }
  todoFind(todoId) {
    axios.get(`/todos/${todoId}.json`).then((response) => {
      this.setState({todo: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  todoUpdate() {
    axios.put(`/todos/${this.props.params.id}.json`,
              {todo: this.state.todo}).then((response) => {
      this.props.history.push(`/${response.data.id}`)
    }).catch((response) => {
      console.log(response)
    })
  }
  formChange(todo) {
    this.setState({todo: todo})
  }
  render() {
    return(
      <div>
        <h2>Edit</h2>
        <TodoForm todo={this.state.todo} buttonLable="Update" onChange={this.formChange.bind(this)} onSubmit={this.todoUpdate.bind(this)} />
        <Link to={`/${this.state.todo.id}`}>Show</Link> |
        <Link to="/">Back</Link>
      </div>
    )
  }
}
EditPage.propTypes = {
  params: PropTypes.object,
  history: PropTypes.object
}
  • IndexPage.js
     ***  上部は同じ  ***

class TodoListItem extends Component {
  render() {
    return(
      <tr>
        <td><Link to={`/${this.props.todo.id}`}>Show</Link></td>
        <td><Link to={`/${this.props.todo.id}/edit`}>Edit</Link></td>  // ★
      </tr>
     )
  }
}
TodoListItem.propTypes = {
  todo: PropTypes.object.isRequired
}
     ***  以下は同じ  ***

削除機能の追加

TodoListItemに 削除用リンクを追加し、クリックした際にバックエンドの削除APIを呼び出す todoDelete メソッドを IndexPageコンポーネントに追加しました。

削除確認のダイアログをどのコンポーネントで出すかは考慮の余地があると思います。今回は確認ダイアログはUIの一部と考え、下位コンポーネントで確認は行うようにしました。 上位コンポーネントで出すという考え方もあると思います。

import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import axios from 'axios'

export default class IndexPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todos: []}
  }
  componentDidMount() {
    this.todoAll()
  }
  componentWillReceiveProps() {
    this.todoAll()
  }
  todoAll() {
    axios.get('/todos.json').then((response) => {
      this.setState({todos: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  todoDelete(todoId) {   // ★
    axios.delete(`/todos/${todoId}.json`).then(() => {
     this.todoAll()
    }).catch((response) => {
     console.log(response)
    })
  }
  render() {
    return(
      <div>
        <h2>List of Todos</h2>
        <TodoList todos={this.state.todos} onDelete={this.todoDelete.bind(this)} />
      </div>
    )
  }
}

class TodoList extends Component {
  render() {
    return(
      <table>
        <thead>
          <tr>
            <th>Due</th>
            <th>Task</th>
            <th colSpan="3"></th>
          </tr>
        </thead>
        <tbody>
          {this.props.todos.map((todo) => (<TodoListItem key={todo.id} todo={todo} onDelete={this.props.onDelete} />))}  // ★
        </tbody>
      </table>
    )
  }
}
TodoList.propTypes = {
  todos: PropTypes.array.isRequired,
  onDelete: PropTypes.func.isRequired  // ★
}

class TodoListItem extends Component {
  destroy(event) {  // ★
    event.preventDefault()
    if (confirm("Are you sure ?")) {
      this.props.onDelete(this.props.todo.id)
    }
  }
  render() {
    return(
      <tr>
        <td> {this.props.todo.due} </td>
        <td> {this.props.todo.task} </td>
        <td><Link to={`/${this.props.todo.id}`}>Show</Link></td>
        <td><Link to={`/${this.props.todo.id}/edit`}>Edit</Link></td>
        <td><Link to='' onClick={this.destroy.bind(this)}>Destroy</Link></td> // ★
      </tr>
     )
  }
}
TodoListItem.propTypes = {
  todo: PropTypes.object.isRequired,
  onDelete: PropTypes.func.isRequired  // ★
}

まとめ

今回までの知識で React.js でアプリ作成は出来るようになるとおもいます。 React.js以外のライブラリーを組みわせるには、もう少し知識がいりますが大抵のことは ドキュメント に書かれていますし、検索をすると Stackoverflow に答えがみつかると思います。

今回、仕事で1万行以上の React.js を書きましたが、Reactの考え方(設計思想)になれるまでは時間がかかりますが、なれれば動きの分かり易いコードが出来る良いライブラリーだと思います。

サーバーサイドプログラマーのためのReact.js 入門 3. いよいよ React.js を書く

いよいよ React.js 入門

開発環境作成に2回も使いましたが、今回から React.js を始めます。

開発環境というかエディターですが、主なエディターは JSX, ES6 をサポートする拡張パッケージ、マクロ等があると思いますので入れておきましょう。もちん WebStorm のようなIDEも良いかもしれません。

これから作るTodoアプリはRuby on Railsのサーバーとhttpプロトコルで通信する必要があります。そこで今回は通信部分には、Promisベースの今風なHTTP clinet ライブラリー axios を使ってみます、 使い方は極めてシンプルです。

インストールは

$ npm install --save-dev axios

まずは、超簡単なReact.js アプリを動かす

前回書いた index.js を以下のように変更してください。もちろんターミナルでは webpack -d -w を実行して下さい。 また、別のターミナルでは rails sRuby on Rails のサーバーも起動しておいて下さい。

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import axios from 'axios'

class IndexPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todos: []}
  }
  componentDidMount() {
    axios.get('/todos.json').then((response) => {
      this.setState({todos: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  render() {
    return (
      <div>
        <h2>List of Todos</h2>
        {this.state.todos.map((todo) => <div>{todo.task}</div>)}
      </div>
    )
  }
}

ReactDOM.render(
  <IndexPage />,
  document.getElementById('example')
)

React.js は 画面をコンポーネント(部品)に分割し、そのコンポーネントは独自のHTMLタグになります。 上では <IndexPage> というコンポーネントを定義しそれを一番下の ReactDOM.render() で index.html にある <div id='example'></div> の中に埋め込むことでコンポーネントが表示されます。

f:id:yuum3:20160225155206p:plain

ブラウザーのConsoleに warnig が表示されています。内容については後で説明しますが、このように React は何かおかしいところがあると warnig を表示してくれます。これは開発・デバックの助けになるのでちゃんと対応して下さい。

解説

state

Reactのコンポーネントは Component クラスを継承して作ります。クラスのコンストラクター(constructor) の中で プロパティー(インスタンス変数)を定義しています。

state は重要なプロパティーで、このコンポーネントが保持する状態やデータなどを保持するオブジェクト(Hashとして使ってます)。ここで todos は サーバーから取得したTodo情報の配列です。適切な(後で説明します)初期値を書いておいて下さい。

render

次に render() メソッドを見てみましょう。これは画面に表示されるHTMLを戻すメソッドです。 JS(JavaScript)の中にHTML タグが書かれていますが、これがJSXで環境設定で入れたBabelがこの部分を通常のJSに変換してくれます。このHTMLタグの部分は JSの式として扱われます。

JSXの中の { 。。。 } にはJSの式が書かれます、この値がタグや属性値として使われます。<div>{todo.task}</div> の todo.task はタスクの文字列になります。

this.state.todos.map の部分は state 内の todos の値(配列)からtask文字列を表示するHTMLの列を生成しています。 ES6を使ってコンパクトに書いてますが、ES5 で書くと

{this.state.todos.map(function(todo) {
  return <div>{todo.task}</div>
})}

サーバーサイドのviewと似た感じですね、サーバーサイドのviewが1ページ全体(またはページ内の一部分)をリクエスト毎に一度に作成するように Reactも必要なタイミングで一度にページ全体を作成するようにプログラミングします。

jQueryなどを使ったAjaxではプログラマーが変更すべき部分のみを書き換えるようにプログラミングする必要がありました。なぜならHTML全体を書き換えるのでは遅く使いやすいUIを実現できないからです。その代償としてコードが煩雑になり、メンテナンスしづらいコードになりがちでした。

Reactは HTML全体を書き換えるようなプログラミンをしてもライブラリーが現在のDOM(HTML構造)と新しいDOM(HTML構造)を比較し、必要な部分のみを書き換えてくれます。

この Virtual DOM という仕掛けが、画面全体を書き換えるというシンプルなプログラミンモデルと性能を両立してくれます。

Reactが生成したHTML(下の画像)を見ると、data-reactid という属性が各タグにふられています、これはVirtual DOMと実際のDOMの管理に使われているのだと思われます。

f:id:yuum3:20160225155215p:plain

componentDidMount (コンポーネントのライフサイクル)

Reactではコンポーネントの表示を明示的な制御はせず Reactライブラリーに任せます。シンプルなコンポーネントは render メソッドだけでも出来ますが、通常はデータを取得したりいろいろな処理が必要になります。そこでReactでは表示処理のあるタイミングで呼び出されるメソッドが決まっています、それを自分でオーバーライトし処理を組み込みます。 詳細はComponent Specs and Lifecycle を見て下さい。凝ったUIを作ったり、React以外のJSライブラリーと組みせる場合などはこのライフサイクルを熟知する必要があります。

現在、Reactを使いアプリを作っていますが、凝った事はしてないので、ほとんどコンポーネント

  • componentDidMount() 最初にコンポーネントが描画された後に呼び出される
  • componentWillReceiveProps(nextProps) コンポーネントが再描画される際に呼び出される(正確には新たなpropsを受け取る時、propsは後で説明します)

の2つで済んでます。 Component Specs and Lifecycle に書かれているように データの取得等はcomponentDidMountメソッドに実装します。

componentDidMountは最初の描画の後に呼び出されます、今回のコードではtodosの初期値は空配列なので、最初は <div><h2>List of Todos</h2></div> が表示されます(もちろん見えませんが)。 その後 componentDidMount が実行され RailsサーバーからTodo情報を取得してきます。 axios の詳細は axiosのGitHub に書かれていますが、見なくとも想像できると思います。

  1. get() でGETリクエストをサーバーに送り
  2. データが取得できると then() に書かれた(無名)関数が実行されます
  3. エラーが起きた場合は catch() に書かれた(無名)関数が実行されます

データが取得できたら this.setState() メソッドが呼び出されていますが Reactではstateの更新はプロパティーへの代入ではなく、このメソッドを呼び出す必要があります。

this.setState() は変更された state を使い再描画を行います。(上のrenderで説明したように Virtual DOM により実際に描かれるのはtodo.task表示のdivタグのみです)

注意点

私が、初めて Reactを使ってハマった点などを書いておきます。

  • 一度表示されたコンポーネントインスタンスがメモリ上に残っているので、他のコンポーネントから再表示を起こしても componentDidMount は実行されません。その時は componentWillReceiveProps が呼び出されます
  • 上にも書きましたが、最初はデータが空(初期値)の状態で描画が起きるので、初期値は重要です。適切な値を設定しておくか、renderの方で空データを考慮しないとnull絡みのエラーが発生します
  • renderで描かれるコンポーネントは基本的に1つのタグである必要があります、今回のコードではHTML的には最初のdivタグはいらないのですが、 このReactの制限から div で括っています
  • List of Todosの画面で発生していた Warning: Each child in an array or iterator should have a unique "key" prop. ... ですが、リストやテーブルのように要素数が可変のタグを正しくReactが認識出るようにkey 属性でユニークな値を設定する必要があります。今回のコードは正しくは以下のように書きます
  ・・・ 省略 ・・・

  render() {
    return (
      <div>
        <h2>List of Todos</h2>
        {this.state.todos.map((todo) => <div key={todo.id}>{todo.task}</div>)}
      </div>
    )
  }
}

 ・・・ 省略 ・・・

Todoリスト表示完成版

Todoリストの表示ページを、

  • ページ全体
  • Todoリスト表示
  • Todo一項目表示

というコンポーネントに分割してみましょう。コンポーネントにはHTMLの属性のように値を渡すことができます。

値を受け取るコンポーネント側では、this.props.属性名 で値をアクセスします。 この props も重要なプロパティーです。また、propsの値をコンポーネント内で変更しないのが React の考え方です。

TodoList.propTypes = { から始まる部分ですが、propの型を宣言する部分です。無くても動作しますが型宣言をしておくと 違う型が渡された場合 waring が発生するので、開発には役立つと思います。

詳細は Prop Validation を参照して下さい

import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import axios from 'axios'

class IndexPage extends Component {
  constructor(props) {
    super(props)
    this.state = {todos: []}
  }
  componentDidMount() {
    axios.get('/todos.json').then((response) => {
      this.setState({todos: response.data})
    }).catch((response) => {
      console.log(response)
    })
  }
  render() {
    return(
      <div>
        <h2>List of Todos</h2>
        <TodoList todos={this.state.todos}/>
      </div>
    )
  }
}

class TodoList extends Component {
  render() {
    return(
      <table>
        <thead>
          <tr>
            <th>Due</th>
            <th>Task</th>
            <th colSpan="3"></th>
          </tr>
        </thead>
        <tbody>
          {this.props.todos.map((todo) => (<TodoListItem key={todo.id} todo={todo} />))}
        </tbody>
      </table>
    )
  }
}
TodoList.propTypes = {
  todos: PropTypes.array.isRequired
}

class TodoListItem extends Component {
  render() {
    return(
      <tr>
        <td> {this.props.todo.due} </td>
        <td> {this.props.todo.task} </td>
      </tr>
     )
  }
}
TodoListItem.propTypes = {
  todo: PropTypes.object.isRequired
}

ReactDOM.render(
  <IndexPage />,
  document.getElementById('example')
)

予告

次回は、Todo情報の変更などに付いて書きます。

サーバーサイドプログラマーのためのReact.js 入門 2. 開発環境の構築の続き

これから作るアプリ

今回と次回で作るアプリですが Ruby on Railsのscaffoldジェネレータが生成する古き良き時代の単純なWebアプリケーションと同じ動きをする、React.js を使った Single page application (SPA)のフロントエンドを作っていきます。

バックエアンド(APIサーバー)は Ruby on Rails を使うことにします。

バックエンドの構築

Ruby on Railsのインストール等はネット上の情報を参考にしてください。 私の会社 EY-OfficeのWikiにもインストール方法を書いています (少し古いのでバージョンは最新のものを入れて下さい)

Railsがインストールできたら、以下のようにプロジェクトを作り scaffold ジェネレータを使い、Railsアプリを作ります。

$ rails new todo_app
$ cd todo_app
$ rails generate scaffold todo due:date task:string
$ rake db:migrate
$ rails server

http://localhost:3000/todosブラウザーからアクセスし、New Todo ページでデータを作成したり、アプリの動きを見てください。

f:id:yuum3:20160215151025p:plain

http://localhost:3000/todos.json をアクセスしてみて下さい。 scaffold ジェネレータが作ったコードはデフォルトでJSONを出力できます、これでバックエンドは完成です!

f:id:yuum3:20160215151031p:plain

rails server で起動したサーバーはReact.js でも使いますので、そのまま動かしておいて、以下の作業は別のターミナルで行ってください。

React開発環境の構築

前回は開発環境を構築する前準備でした、今回は具体的な開発環境を作り開発してみます。

Reactに行く前に

Railsの環境ではスタティックなコンテンツ等は public/ に置きます。

そこで JS が入るディレクトリーとJSを表示するための index.html を作成ておきます。

$ mkdir public/js
$ vi public/index.html
  • index.html
<!DOCTYPE html>
<html>
<head>
  <title>ReactTodo</title>
  <meta charset="utf-8">
</head>
<body>
  <div id="example"></div>
  <script type="text/javascript" src="js/app.js" charset="utf-8"></script>
</body>
</html>

React開発環境の構築

今回は Railsプロジェクトの中に frontend ディレクトリーを作りそこで開発することにします。npm init でいろいろとたずねれますが、気にせず return していっても良いです。

$ mkdir frontend
$ cd frontend
$ mkdir -p src/js src/css
$ nam init -y
  • React.js、babel、ESLint、webpack のインストール。 XXXX-loader はwebpack用のモジュールです。

本日ためしたところ eslint 2.0.0 をインストールすると babelが動作しませんでしたので @1.10 でバージョン 1.10.X をインストールしています。非互換な変更があったようです

$ npm install --save react react-dom
$ npm install --save babel-loader babel-preset-es2015 babel-preset-react
$ npm install --save eslint eslint-loader eslint-plugin-react babel
$ npm install --save css-loader style-loader
$ npm install -g webpack 
  • babel 設定ファイル .babelrc の作成。ES6(正式にはES2015)とJSX(React)の変換を行います
{
  "presets": ["es2015", "react"]
}
  • ESLint 設定ファイル .eslintlrc の作成、後で説明します。
{
    "env": {
        "browser": true,
        "node": true,
        "es6": true
    },
    "parserOptions": {
        "sourceType": "module",
        "ecmaFeatures": {
            "experimentalObjectRestSpread": true,
            "jsx": true
        }
    },
    "extends": ["eslint:recommended", "plugin:react/recommended"],
    "plugins": [
        "react"
        ],
    "rules": {
        "strict": [2, "function"],
        "no-undef": 2,
        "no-console": 0,
        "arrow-parens": [2, "always"]
    }
}
  • webpack の設定ファイル webpack.config.js の作成
    • entry: で生成物 app.js が src/js/index.js から作られることを定義
    • output: で生成物の出来る場所を定義、Rails の public/js/index.js に置かれます
    • PreLoaders: で生成前に ESLintの実行を指定
    • loaders: で生成(変換)作業を定義
      • .js, .jsx に対しては babelで変換
      • .css に対しては、styleタグ追加、css内のurl()等の解決を指定しています
    • resolve: は require/import での拡張子の省略を定義
    • eslint: eslintの設定ファイルを定義しています

設定ファイルの詳細は webpackのドキュメント や各ローダーのドキュメントを見てください。日本語の記事もいくつかありますので、両方を見ながら覚えて行って下さい。 (いずれ記事を書きたいですが、私もまだまだ苦労しています・・・)

module.exports = {
  entry: {
    app: "./src/js/index.js"
  },
  output: {
    path: '../public/js',
    filename: "[name].js"
  },
  module: {
    preLoaders: [{
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loader: "eslint-loader"
    }],
    loaders: [{
      test: /\.css$/,
      loader: "style!css"
    }, {
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loader: 'babel-loader'
     }]
  },
  resolve: {
    extensions: ['', '.js', '.jsx', '.css']
  },
  eslint: {
    configFile: './.eslintrc'
  }
};

試してみよう!

やっと環境ができました、さっそく試してみましょう。 まずは、 Reactの Getting Started のコードを試してみましょう

  • src/js/index.js
var React = require('react');
var ReactDOM = require('react-dom');

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('example')
);

ターミナルで webpack -d -w を入力し、少し待つと以下のように表示されます

  • app.js とマップファイルが生成されました。
  • また、index.js では React が定義されているが使われてないというエラーが出ています。
$ pwd
/Users/XXXXXX/todo_app/frontend
$ webpack -d -w                                 
Hash: cf1445dbfe9ce6c676f1
Version: webpack 1.12.13
Time: 2268ms
     Asset    Size  Chunks             Chunk Names
    app.js  764 kB       0  [emitted]  app
app.js.map  846 kB       0  [emitted]  app
    + 180 hidden modules

ERROR in ./src/js/index.js

/Users/yy/tmp/todo_app/frontend/src/js/index.js
  1:8  error  "React" is defined but never used  no-unused-vars

✖ 1 problem (1 error, 0 warnings)

ここでは、ESLintの出したエラーは一旦無視(他のエラーはちゃんと追求して下さいね)してブラウザーhttp://localhost:3000 をアクセスすると 画面に Hello、wold! が表示されます。

f:id:yuum3:20160215151043p:plain

上手くいかない場合はブラウザーのコンソール(開発ツール、インスペクター)を見てください。エラーや警告が出ているかもしれません。

上で書いたJSは ES5 ですが ES6 に変えてみましょう。同じエラーは発生しますが Hello, world! が表示されるはずです

import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('example')
)

Next Steps のコードも試してみてください。

ESLint

JavaScriptは動的な言語です、従って実行してみるまでエラーが発見できません。 テストを書いてこまめに開発していていれば良いのですが、ブラウザーを操作してやっと開発中のページを実行した途端に、つまらない文法エラー発生! などということになりがちです。

そこで大きな助けになるのが ESLint です。文法エラー、未定義エラー、定義されているが未使用などを実行前にチェックしてくれるので開発がはかどります!! 特に Reactでは JS の中に HTML が書かれる JSX は、なれるまで間違いが発生しやすのでお勧めです。

ただし、JSは動的な言語なのでチェックは完璧にはできません。上の Hello,world! 出ていたエラーは Reac変数がプログラムの中で使われていないので発生しました。しかし、この行を消してしまうと React.js が読み込まれないので実行できません。

このような場合は、ソースコードか設定ファイルに指定することで回避できます。ここでは .eslintlrc に React は使われてなくても良いというrule を設定を追加してみましょう。 詳しくは Configuring ESLint をみてください。

{
    ... ここまでは同じ...
    "rules": {
        "strict": [2, "function"],
        "no-undef": 2,
        "no-console": 0,
        "arrow-parens": [2, "always"],
        no-unused-vars: [2, {"varsIgnorePattern": "^React$"}]
    }
}

次回は、いよいよ React.js ^^)/

サーバーサイドプログラマーのためのReact.js 入門 1. 開発環境の構築

最近、バリバリと jQueryベースのフロントエンドを React.js に置き換えています。

私も主に Ruby on Rails等の サーバーサイドエンジニアで、最近のフロントエンド開発を本格的に開発するのは初めてです。いろいろとつまずきながら進んできました。

f:id:yuum3:20160204101138p:plain

まずは node.js をインストールし npm を知る

環境構築のためのツールは node.js の上で動きます、そこで node をインストールします。 ツールを動かすだけなので node のバージョンはあまり気にしなくて良いと思います。(2016-02-04日現在ではバージョン5.5.0が入ります)

$ brew install node

node をインストールすると npm コマンドもインストールされます。npmは node.js用ライブラリーと、その管理を行うコマンドです。 Rubyの gem に相当するものですが、さらに Ruby の bundle, rake 的な機能も持っていて node の世界を極めるには非常に重要なものです。

node の生態系は ruby と似ていますが、さらに改良されています。良く使うコマンドは

  • npm init カレントディレクトリーにnpm用設定ファイル package.json ファイルを作ります。このファイルにはプロジェクト(ライブラリー)の情報、依存ライブラリーなどの情報が入っています。コマンドを起動すると name: version: などを尋ねてきますが後で package.json ファイルを変更できるので全てreturnでも構いません。
  • npm install --save ライブラリー名 ライブラリーカレントディレクトリー下に(node_modulesディレクトリーを作り)インストールします。 --save を指定すると依存ライブラリーとしての情報が package.json に入ります。
  • npm install -g ライブラリー名 ライブラリーを共通な場所(brewでnodeをインストールした場合 /usr/local/lib/node_modules)にインストールします。コマンドを含むものは -g (--global) で入れるとコマンドもしかるべき場所に入り便利です。
  • npm --help npm にはたくさんの機能があります、知りたい場合はヘルプを

ビルドツールを選ぶ(知る)

フロントエンドの生態系は凄い速さで進化しています。フレームワークも少し前は AngularJS とか言われてましたが、一昨年あたりから React が主役っぽいし、Reactは表示ライブラリーなので、それを使うためのフレームワークは Flux だとか Redux だとか出来きてます・・・

ビルドツールのようなものには grunt, gulp, babel, bower, browserify, webpack ... とたくさんのもので賑わっていて、何を使えば良いのか迷います。近くにフロントエンドのエンジニアがいれば相談するのが良いと思いますが、やみくもにネットを調べても発散するばかりです !

やりたいことを整理しましょう

まず開発環境の要件というか、やりたい事をまとめましょう。今回の私の要件は

  • React を使う
  • Redux を使うのかな?
  • 現在のJS(ES5)はだるいので、ES6 を使いたい
  • 完全なシングルページアプリ (Rails等のviewは使わない)
  • ブラウザーは最新でけサポートすれば良い
  • 環境構築ツールはシンプルなもにしたい、多少の不便があっても良い
  • HTML,CSSプログラマーが書く(既に出来たHTML/CSSがある)
  • 過去の開発環境にはこだわらない
  • 過去の(デプロイされた)ディレクトリー構成にはこだわらない

一般論としての良いツールではなく自分のやりたい事に最適なツールを探しましょう

ネットで調べてみる

ビルドツール等に関しては qiita を始め多数の日本語の情報がネットから得られので概要を理解するのに役立ちます。しかし最新の正確な情報は本家(英語)のページを読みましょう。

JSX, ES6

ReactではJSXというJSの中に HTML を埋め込んだものを使ういます(使わない方法もあるようですが)。JSXはそのままでは実行できないので通常のJSに変換するツールが必要になります。

var React = require('react');
var ReactDOM = require('react-dom');

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('example')
);

また、ES6(正確には ES2015, ECMAScript2015)は全ての主要ブラウザーでサポートされているわけではありません。そこでES6からES5に変換する Babel が良く使われています。

JSXやBabelで変換されたコードをブラウザーで動かすのは良いのですが、デバックはどうするのでしょうか? 変換されたコードでデバックするのでしょうか? 安心して下さい、そのために元のコードでデバックするための ソースマップ(.map) が主要ブラウザーではサポートされています。

ビルドツール

現時点では BrowserifyWebpack が良いようです。

Browserify

Browserify はもともと node.js 用のモジュール構造をブラウザーのJSで使えるようにする技術ですが、それを行うための変換ツールでもあります。また、babelやJSXの変換ツールを統合できます。

Browserifyとnpmを使った最小限のビルドツールの作り方が Reactをnpmでビルドする方法 browserify (watchify) + babelify編 にまとめられています。

npm のスクリプト実行機能を使い以下のように browserify(watchify), babelify を使い ES6(babel-preset-es2015), JSX(babel-preset-react)の変換を行います。また、ソースマップ作成にexorcistを使っています。

watchify -t babelify ./app.js -o 'exorcist ./build.js.map > ./build.js' -d

Browserifyはいつかのツールを組み合わせボトムアップにビルド環境を作るツールです。各種ツールgulpと組み合わせると、自分のやりたいことが実現できると思います。

Webpack

Webpack ホームページの絵からもわかるようにフロントエンドの統合開発ツールです。フレームワークのような作りになっていて、そこに種々のツールプラグインで組み込み設定ファイルで管理します。

新たにトップダウンで作られたツールなのでわかり易いように思います。ただし、自分のやりたいことが完全に実現できないこともあるかもしれません。

試してみた

BrowserifyとWebpackのどちらが良いのか情報でけでは決めかねたので ReactのGetting Started を両方の環境で試してみました。

その結果、今回の要件にはWebpackが良さそうなので使うことにしました。

予告

次回はReactを使った簡単なアプリ作成を Webpack の環境作りから始めます・・・・

Network Link ConditionerのVery Bad Networkは役立つ!

先日、あるサービスでトラブルが !! お客様のところで、ファイルのアップロードが出来ないと連絡がありました。

こちらで試してみると正常にアップロードできます。詳しく聞くと、お客様の自宅からアップロード出来ないとのこと・・・

f:id:yuum3:20160128094525p:plain

Macにはネットワークの速度を制限するツールがある事を思い出し、Network Link Conditioner をインストール ( 詳細はこのページでも参考にして下さい ) し通信速度 3G, Edgeなどにを落としてもアップロードできます。

Profileを見ると Very Bad Network というのがあります、Packets Dropped(パケットロス)やDelay(遅延)に値が設定されています、通信品質が極めて悪いネットワークの設定ですね。

これで、アップロードを試すと確かに全然終わりません !! アクティビティ モニターでネットワークをモニターすると、超低速でアップロードが実行されているようです(転送レートから計算すると10分くらいすれば完了しそうでしたが・・・・)。

ということで、今日の結論 「Network Link ConditionerのVery Bad Networkは役立つ」

Turnip, Cucumber などを使った end-to-endテストのテストデータに付いて

Turnip, Cucumber, RSpec feature などを使った end-to-end テスト のテストデータをどう作るかについて、今までの経験を書いてみます。

RSpecなどを使ったモデルのテストであれば、テストデータは factory_girl 等を使い、テストに必要なデータを it, describe単位でテストで作成する事が多いと思います。

f:id:yuum3:20160108112329j:plain

しかし、end-to-endテストでは人がそのアプリを使う手順、シナリオに沿ってのテストにり、モデルのテストのように個々のテストで個別のデータを必要になることはあまりなく、いくつかのデータを作っておけば済みます。また、モデルのテストでは必要になるモデル(テーブル)は少数ですがend-to-endテストではほとんどのモデル(テーブル)にデータが必要になります。

そこで、今まで作ったアプリのend-to-endテスト用のデータをどのように準備したかを、まとめてみました。またテストデータではありませんが、開発時に使う db_seed についてもふれています。

1. Cucumber + fixture + db_seedなし

Rails 2の頃に作ったアプリで標準の fixture を使っています。なんとモデルも fixture を使っているので fixture はかなりカオスな状況になっています ^^;

また、db_seed については知らなかったので、 rake db:fixtures:load を使っていました。 fixture は人手でデータを作るので テストや開発に必要な意味のあるデータを書くのには向いていると思います。 ただし、テスト用にデータを100件準備するとかには向きません(ERBが書けるので出来ないわけではないですが)。

2. RSpec feature + factory_girl + db_seed

この開発では end-to-endテストは RSpec feature を使い、テスト用のデータは factory_girl を使って作っていますが、helperメソッド内で全モデル用のデータを作り全end-to-endテストで同じものを使っています。 db_seed はActiveRecordメソッドだけで別に作りました。

3. RSpec feature + fixture + db_seedなし

これは、1. のシステムをRails4にバージョンアップする際に管理画面の end-to-endテストを追加した際に作りました。従ってテストデータは 1. と同じです。

4. RSpec feature + factory_girl + factory_girl を使ったdb_seed

db_seed と end-to-endテストのデータは同じもで良いのでは? または db_seed は end-to-endテストのデータ + アルファ なのでは? という仮定のもとに db_seedもfactory_girl を使って作りました。 同じようなものを2つ作らずに済んだので良いアプローチだったと思います。

また、end-to-endテストを作る際には実際にブラウザーを操作し、画面や DOM を見ながらテストを作るのでdb_seed と end-to-endテストのデータが同じだと作業がはかどりました。

5. Turnip + factory_girl + db_seed

この案件は既存のアプリの改善だったので、既に db_seed はあるが end-to-endテストは無い状態だったので end-to-endテストのデータは 2. と同じように作りました。

ただし、 4. に書いたようにテストを書く際にテスト用データでアプリを動かせると便利なので、それを行うツールを作りテストアプリ作成に役立てました。

ツールは以下のようなコードで、load_turnip_seed で Turnip 用に書いたテストデータ作成メソッド turnip_seed_data() を呼び出していま。turnip_seed_data() は通常のfactory_girlを使ったコードです。 テストデータを消すための truncate というメソッドも作っておきました。

# Usage:
# rails runner Tasks::DbTool.truncate
# rails runner Tasks::DbTool.load_turnip_seed

$LOAD_PATH << "#{Rails.root}/spec"  # TODO: もっと良いやり方
require 'turnip_seed_data'

class Tasks::DbTool
  extend FactoryGirl::Syntax::Methods

  def self.load_turnip_seed
    self.truncate
    turnip_seed_data()
  end

  def self.truncate
    ActiveRecord::Base.connection.tables.reject{|t| t == 'schema_migrations'}.each do |table|
      # MySQL only
      ActiveRecord::Base.connection.execute("TRUNCATE TABLE `#{table}`")
    end
  end
end