React.js + Photon.css + Webpack で Electronのサンプルアプリを作ってみた

React.js + Photon.css + Webpack で Electronのサンプルアプリを作ってた時のメモです。

f:id:yuum3:20160523105315p:plain

開発環境

まず、以前書いた サーバーサイドプログラマーのためのReact.js 入門 2. 開発環境の構築の続き のように React.js の開発が出来る環境を準備します。

追加するのは electron-prebuiltMac OS X風なUIが作れる Photon を追加するのみです。

npm install photon で入る Photon は違うものなので注意して下さい。

  • インストール
$ npm install --save react react-dom
$ npm install --save-dev babel-loader babel-preset-es2015 babel-preset-react
$ npm install --save-dev eslint eslint-loader eslint-plugin-react babel
$ npm install --save-dev css-loader style-loader
$ npm install --save isomorphic-fetch
$ npm install -g webpack

$ npm install --save-dev electron-prebuilt
$ npm install --save https://github.com/connors/photon
$ npm install --save-dev url-loader file-loader
  • webpack.config.js は
module.exports = {
  entry: {
    "app": "./src/js/index.js",
  },
  output: {
    path: './build/',
    filename: "[name].js"
  },
  module: {
    preLoaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "eslint-loader"
    }],
    loaders: [{
      test: /\.css$/,
      loader: "style!css"
    }, {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader'
    }, {
      test: /\.(eot|woff|woff2|ttf|svg|png|jpg)$/,
      loader: 'url-loader?limit=30000&name=[name]-[hash].[ext]'
    }]
  },
  resolve: {
    extensions: ['', '.js', '.css']
  },
  eslint: {
    configFile: './.eslintrc'
  }
};
  • package.js に以下の設定を追加(変更)します
  "scripts": {
    "start": "electron main.js"
  },
  • main.js は ElectronのQuick Start からコピーし windowサイズを変更します

  • index.html は photon 用の class を指定しましたが、良くある React.js 用の index.html です。

<!DOCTYPE html>
<html>
<head>
  <title>じゃんけん ポン!</title>
  <meta charset="utf-8">
</head>
<body>
 <div id="example" class=window"></div>
  <script type="text/javascript" src="build/app.js" charset="utf-8"></script>
</body>
</html>
  • src/js/index.js は通常の React.js のコードです photonを使うために import 'photon/dist/css/photon.css' を追加したくらいです。

開発コマンド

Electron の開発は electron コマンドを起動する アプリのウィンドウが起動され、リロード(⌘+R) でコードが再ロードされるので開発できます。ただし electron コマンド は動き続けるので バックグラウンドで起動しました。

$ nam start &
$ webpack -d -w

今回つくったサンプルアプリ

React.js紹介セミナー 用に作ったサンプルコードは 私のGitHub を clone し electron ブランチです。 他のブランチには React Native 等のコードもあります ^^)

楽々GitLabサーバー作成手順

教育の仕事でGitLab(プライベートでpull requestなどが出せる安いサービス)が必要になり、サーバーを立ち上げました。以前は自社のコードもGitLabで管理していたのですが、今は 改造版Ginatra を使っているので、教育の期間のみGitLab用のサーバーを立ち上げる事にしました。

f:id:yuum3:20160516151328p:plain

GitLabのインストール

以前はGitLabのインストールはたいへんでしたが、今は apt や yum でインストールできます。 IaaSクラウドサービスでサーバーを準備し、インストールすれば簡単に完了です。 RDB(PostgreSQL), nginx 等もインストールされます。

私は Ubuntu が慣れているので、Ubuntu 14.04 にインストールしましました。

$ sudo apt-get update
$ sudo apt-get -y dost-upgrade

$ sudo apt-get install curl openssh-server ca-certificates postfix
$ curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
$ sudo apt-get install gitlab-ce
$ sudo gitlab-ctl reconfigure

SSL化しよう

この手のサーバーは今時 https で運用しますよね。 ということで無料でつかえる Let’s Encrypt を使う事にしました。有効期間は3ヶ月ですが教育期間は休みを含め 2ヶ月なので 更新作業も不要ですね ^^)

作業手順は Qiita にありました。ほぼこのままです。

$ git clone https://github.com/letsencrypt/letsencrypt.git
$ cd lets encrypt
$ ./letsencrypt-auto  --help      # installが行われる
$ sudo vi /etc/gitlab/gitlab.rb
---- custom_gitlab_server_config を変更
nginx['custom_gitlab_server_config'] = "include /etc/letsencrypt/nginx.conf;"
---

$ sudo mkdir /etc/letsencrypt
$ sudo vi /etc/letsencrypt/nginx.conf
--- 以下を追加
location ^~ /.well-known {
    alias /var/letsencrypt/.well-known;
}
---

$ sudo mkdir /var/letsencrypt
$ sudo gitlab-ctl reconfigure
$ ./letsencrypt-auto certonly --webroot --webroot-path /var/letsencrypt -d gitlab.ey-office.net
$ sudo vi /etc/gitlab/gitlab.rb
--- 以下の行を変更
external_url 'https://gitlab.ey-office.net'    # サーバーのURLを指定
nginx['redirect_http_to_https'] = true
nginx['ssl_certificate'] = "/etc/letsencrypt/live/gitlab.ey-office.net/fullchain.pem"
nginx['ssl_certificate_key'] = "/etc/letsencrypt/live/gitlab.ey-office.net/privkey.pem"
---

$ sudo gitlab-ctl reconfigure

デモなどで使えるGitコミットを簡単にresetし切替えるツールを作りました

私は仕事がら、人前でコード作成のデモを行う事がよくあります。その場でバリバリとライブ・コーデイング出来るとカッコ良いですが、間違えたり、コードを書くことに気を取られ説明がおろそかになったりすることも起こりがちです。

f:id:yuum3:20160506094523p:plain

そこで説明のストーリに沿ってコード作成の過程をgitにコミットしておき、コマンドで簡単に git reset を行い、その時点でのコードを説明したり、動かしたりできるツールを作りました。

上の画像のように、このコマンドを起動するとgit logが表示され、現在のコミットが赤い色で表示されます、ここで n キーを押すと次のコミットに git reset できます。また p キーで前のコミットに git reset したり、カーソル上下キーでコミットを移動して RETURNキーでそこにgit reset できます。

このコマンドを実行すると git reset されてしまうので、 -c オプション指定で 予めファイルに全git logを書いておきます。

rubyで作ってあります、 gem 等のインストールは不要です。

#!/usr/bin/env ruby

require 'io/console'

GIT_LIST_PATH = "#{ENV['HOME']}/tmp/git-demo.lst"

RED = "\e[31m"
YELLOW = "\e[33m"
BLACK = "\e[30m"
FIN = "\e[0m"

UP = "\e[A"
DOWN = "\e[B"
RETURN = "\r"


def show_log_item(item, current)
  if current
    print "#{RED}#{item[0]} #{BLACK}#{item[1]}#{FIN}#{RETURN}"
  else
    print "#{YELLOW}#{item[0]} #{BLACK}#{item[1]}#{FIN}#{RETURN}"
  end
end

def show_git_log(log, current_index)
  log.each_with_index do |item, ix|
    show_log_item(item, ix == current_index)
    puts
  end
end

def get_current_hash
  IO.read("|git log --pretty=format:'%h' -1")
end

def find_current_hash(log, current_hash)
  log.find_index {|item| item[0] == current_hash}
end

def wait_key
  key = STDIN.getch
  if key == "\e" && STDIN.getch == "["
    case STDIN.getch
    when "A" then ""
    when "B" then ""
    else ""
    end
  else
    key
  end
end

def cursor_up
  print UP
  STDOUT.flush
end

def cursor_down
  print DOWN
  STDOUT.flush
end

def set_cursor(log, current_index)
  (log.size - current_index).times { cursor_up }
end

def reset_cursor(log, current_index)
 (log.size - current_index).times { cursor_down }
end

def move_current_postion(log, index, delta)
  show_log_item(log[index], false)

  if delta > 0
    if index < log.size - 1
      cursor_down
      index += 1
    end
  else
    if index > 0
      cursor_up
      index -= 1
    end
  end

  show_log_item(log[index], true)
  index
end

def get_git_log
  IO.readlines(GIT_LIST_PATH).map {|s| s.scan(/^(\w+) (.*)\n?/)[0]}
end

def put_git_log
  system "git log --pretty=format:'%h %s' > #{GIT_LIST_PATH}"
end

def git_reset_hard(log, index)
  system "git reset --hard #{log[index][0]}"
end

def main
  log = get_git_log
  current_index = find_current_hash(log, get_current_hash)
  show_git_log(log, current_index)
  set_cursor(log, current_index)
  do_reset = false

  while true
    case wait_key
    when 'n'
      current_index = move_current_postion(log, current_index,  - 1)
      do_reset = true
      break
    when 'p'
      current_index = move_current_postion(log, current_index,  + 1)
      do_reset = true
      break
    when "\r", "\n"
      do_reset = true
      break
    when "\C-c", "q"
      break
    when ""
      current_index = move_current_postion(log, current_index,  - 1)
    when ""
      current_index = move_current_postion(log, current_index,  + 1)
    end
  end
  reset_cursor(log, current_index)
  git_reset_hard(log, current_index)  if do_reset

end

if ARGV.size > 0
  if ARGV[0] == "-c"
    put_git_log
    puts "#{GIT_LIST_PATH} created."
  else
    puts "#{$0} -c"
    puts "     or  "
    puts "#{$0} "
    puts "  n: git reset --hard Next commit"
    puts "  p: git reset --hard Pervious commit"
    puts "  ↑ ↓: seek commit"
    puts "  RETURN: git reset --hard Current commit"
    puts "  q: quit"
  end
else
  main
end

Gistにも置きました

React.jsの紹介的なセミナーを行います

React.jsの開発も終わり、得られた知見をもとに 5月23日 に React.jsの紹介的な無料セミナーを行います。

atnd.org

内容は、React.js がなぜ良いのか、開発環境について、簡単なアプリをライブで作成・・・ などの React.jsの紹介的な内容になります。

会場は人材系の会社で、終了後に軽食の出る懇親会をスポンサーしてもらっています。人材系の会社ですがブラックな会社ではないので安心して御参加ください。

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情報の変更などに付いて書きます。