Rails5(ActionCable)とReact-railsで簡易チャットアプリを作ってみる

# はじめに

Rails5とActionCableとreact-railsを使って簡易チャットアプリを作りました。
それぞれ写経したものを組み合わせた感じです　
突っ込みどころ満載だと思いますが、ご容赦下さい

# 実行環境

* Ruby 2.3.0
* Rails 5.0.0.beta3
* react-rails 1.6.2
* redis 3.2.2

# 参考記事

* [react-railsを使ってReactのTutorialをやってみる](http://qiita.com/joe-re/items/96f12dda4a62470d1d7c)
* [Rails 5 + ActionCableで作る！シンプルなチャットアプリ（DHH氏のデモ動画より）](http://qiita.com/jnchito/items/aec75fab42804287d71b)

# 準備
- Rubyのバージョンを確認(2.2.1以上になっているか)

## アプリケーションの作成
```
# bundle install は一旦スキップで
rails new rails5-app --skip-bundle
```

## Gemを追加
```
# react-rails
gem 'react-rails'

# redisのコメントアウトを外す
gem 'redis', '~> 3.0'
```

#### bundle install
```
bundle install --path vendor/bundle
```

#### migrate(特にDBを使用する予定はありませんが・・・)
```
# rails5からコマンドがrakeからrailsに変わっているので注意
bundle exec rails db:craete
bundle exec rails db:migrate
```

## react-railsをインストール
```
bundle exec rails g react:install
```

## コントローラを作成
```
# showアクションを持つroomコントローラ
bundle exec rails g controller room show
```

## チャンネルを作成
```
# speakアクションを持つroomチャンネルを作成
bundle exec rails g channel room speak
```

## コンポーネントを作成
```
bundle exec rails g react:component CommentBox
```

# サーバ側の実装

## チャンネルの実装
```ruby:app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  # おまじない（調査中です）
  def subscribed
    stream_from "room_channel"
  end

  # 省略

  # クライアントから来たspeakをハンドリング
  def speak(data)
    ActionCable.server.broadcast 'room_channel', comment: data['comment']
  end
end
```

## ルーティングを設定
```ruby:config/route.rb
Rails.application.routes.draw do
  root 'rooms#show'

  # コメントアウトを外す
  mount ActionCable.server => '/cable'
end
```

# クライアント側の実装

## ActionCableを有効に

```javascript:app/assets/javascripts/cable.coffee
# コメントアウトを外す
@App ||= {}
App.cable = ActionCable.createConsumer()
```

## Railsのviewに追記

```html:app/views/rooms/show.html.erb
<%= react_component('CommentBox')%>
```

## コンポーネントの実装（長いので分けます）

```javascript:app/assets/javascripts/components/comment_box.js.jsx
var CommentBox = React.createClass({
  getInitialState: function() {
    return {comments: []};
  },
  componentDidMount: function() {
    this.setupRoomChat();
  },
  updateCommentList: function(comment) {
    comment.id = new Date().getTime();
    this.setState({ comments: this.state.comments.concat([comment]) });
  },
  setupRoomChat: function() {
    App.room = App.cable.subscriptions.create("RoomChannel", {
      connected: function() {},
      disconnected: function() {},
      received: function(data) {
        // サーバからメッセージを受信
        this.updateCommentList(data.comment);
      },
      // ここが1番謎です（解明中）
      updateCommentList: this.updateCommentList.bind(this),
      speak: function(comment) {
        return this.perform('speak', {
          comment: comment
        });
      }
    });
  },
  handleCommentSubmit: function(comment) {
    // サーバにメッセージを飛ばす
    App.room.speak(comment);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Chat Room</h1>
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
        <CommentList comments={this.state.comments} />
      </div>
    );
  }
});
```

```javascript:app/assets/javascripts/components/comment_box.js.jsx
// ...省略
var CommentForm = React.createClass({
  onClick: function(e) {
    e.preventDefault();
    var text = ReactDOM.findDOMNode(this.refs.text).value.trim();
    if (!text) {
      return;
    }
    this.props.onCommentSubmit({text: text});
    ReactDOM.findDOMNode(this.refs.text).value = '';
    return;
  },
  render: function() {
    return (
      <div className="commentForm">
        <div>
          <input type="text" placeholder="Say something..." ref="text" />
        </div>
        <div>
          <button type="button" onClick={this.onClick}>Post</button>
        </div>
      </div>
    );
  }
});

```

```javascript:app/assets/javascripts/components/comment_box.js.jsx
// ...省略
var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.comments.map(function (comment) {
      return (
        <Comment key={comment.id}>
          {comment.text}
        </Comment>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        {this.props.children}
      </div>
    );
  }
});
```

# 実行
```sh
bundle exec rails s
```

# 確認
ブラウザから`localhost:3000`にアクセス
2窓で上手くチャット出来ますでしょうか

# 最後に
ReactのRの字もわからないのでFluxも何を使っていません。
怪しい箇所が何ヶ所もあるので何かあれば教えて頂けると幸いです

# github
一応ここにあります。
<https://github.com/TekkaMK/rails5-react-chat>