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

React Nativeアプリを別のMacで動かそうとしてハマった メモ

あるセミナー向けに作った、React Nativeアプリを別のMacで git clone して実行しようとしたら 下の画像のエラーが出て解決まで時間がかったのでメモしておきます。

f:id:yuum3:20160523142141p:plain

React Native 開発環境の構築

React NativeのGetting Startedにある手順で

$ npm install -g react-native-cli
$ brew install watchman
$ brew install flow

インストールされた react-nativeコマンドでプロジェクトを作成

$ react-native init react_native

ボタンやTabBarのアイコンが使いたかったので、以下をインストール。 react-native-vector-icons をプロジェクトに設定するにのrnpm (React Native Package Manager) を利用

$ npm install --save react-native-button
# npm install --save react-native-vector-icons 

$ npm install rnpm -g
$ rnpm link

ここで react-native run-ios を実行すると、iOS用コードのコンパイルや別ウインドーに React packager が起動され、しばらくすると iOS Simulator が起動されるます。 後は index.ios.js を修正し、 iOS Simulator で⌘+R でリロードする事で開発が進められます。完成したものを GitHubに置きました。

Macでの作業

別のMac で git clone し

$ npm install -g react-native-cli
$ brew install watchman
$ brew install flow

$ nam install
$ react-native run-ios

iOS Simulator が起動されるのですが、上のような赤い画面が表示されました

画面に書かれている issue を眺め

  1. watchman watch-del-all
  2. rm -rf node_modules && nam install
  3. rm -rf $TMPDIR/react-packager-*

を実行しても同じ・・・・

落ち着いてターミナルを見ると

npm WARN react-native@0.26.1 requires a peer of react@15.0.2 but none was installed.

通常この手のワーニングが出っていても動く事が多いのですが、調べてみると react@15.1.0 がインストールされていました。そこで

$ npm uninstall --save react@15.1.0
$  npm install --save react@15.0.2

したところ、ワーニングは消え、

iOS Simulator で無事にアプリが起動されました ^^)/

このサンプルアプリは 私のGitHub react-native ブランチにあります。

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の考え方(設計思想)になれるまでは時間がかかりますが、なれれば動きの分かり易いコードが出来る良いライブラリーだと思います。