React.jsチュートリアルをReact.rbで書いてみる


ということでいつものシリーズです。今回はちょっと異色かも。

## [React.rb](http://reactrb.org/) って何？

[Opal](http://opalrb.org/) を知っていますか？ Ruby を JavaScript にコンパイルしてくれるコンパイラです。Opal を使えば JavaScript のアレとかコレとかそういったことにイライラすることはなく、Ruby でさくっと JavaScript を実装できます。Rails や Sinatra にも対応しているので安心です。

そんな Opal 上で動く 100% pure Ruby な React.js の実装が React.rb です。ええ、**パクリ**です。Web サイトもドキュメントも全てパクリです。オリジナルの React.js のサイトやドキュメントが CC BY 4.0 という原作の著作権表示さえしていればパクってもいいっていうライセンスなので、本当にパクっています。サイトデザインだけではなく、HTMLのソースレベルでパクってます。本当にありがとうございました。

そんな Reat.rb ですが、Opal と一緒に使うと、Ruby だけで React.js に相当する素晴らしいビューを実装できます。Rails や Sinatra にも組み込めるので、安心です。これで、JavaScript とはおさらばできるというものです。

## チュートリアルの作成

### 準備する

さっそくチュートリアルを書き換えていきましょう。[React Tutorial](https://github.com/reactjs/react-tutorial) はあらかじめクローンしておいてください。

本来は Rake でタスクを作ってコンパイルするのですが、今回は [inline-reactive-ruby](https://github.com/reactive-ruby/inline-reactive-ruby) を使います。コレを使うと、Ruby で書いたコードをブラウザ上でコンパイルすることができます。先ほどのサイトに書いてあるリンク先から、 inline-reactive-ruby.js をダウンロードして `public/scripts/nline-reactive-ruby.js` としておいて置きます。

### index.htmlを書き換える

inline-reactive-ruby.js を追加します。また、jQuery が必要なので、残しておきます。marked.min.js は同じ用途で使うので、これも残しておきます。後はいりません。

script の type は "text/ruby" です。inline-reactive-ruby.js によって、自動的に JavaScript に変換され、読み込まれます。このあたりは Babel を使ったオリジナルと一緒です。

```public/index.html
<!DOCTYPE html>
<html>
  <head> 
    <meta charset="utf-8">
    <title>React Tutorial</title>
    <!-- Not present in the tutorial. Just for basic styling. -->
    <link rel="stylesheet" href="css/base.css" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script>
    <script src="scripts/inline-reactive-ruby.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/ruby" src="scripts/example.rb"></script>
    <script type="text/ruby">
      # To get started with this tutorial running your own code, simply remove
      # the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>
```

### ソースを書く

では、オリジナルの JSX を Ruby に書き換えていきましょう。

```public/scripts/example.rb
# This file provided by Facebook is for non-commercial testing and evaluation
# purposes only. Facebook reserves all rights not expressly granted.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require 'opal'
require 'browser'
require 'browser/interval'
require 'browser/delay'
require 'browser/console'
require 'opal-jquery'
require 'reactive-ruby'

class Comment < React::Component::Base
  param :author, type: String

  def raw_markup
    { __html: `marked(#{children.join}, {sanitize: true})` }.to_n
  end

  def render
    div.comment do
      h2.commentAuthor { params.author }
      span(dangerouslySetInnerHTML: raw_markup)
    end
  end
end

class CommentBox < React::Component::Base
  param :url, type: String
  param :pollInterval, type: Numeric

  def load_comments_from_server
    HTTP.get(params.url) do |response|
      if response.ok?
        state.data! response.json
      else
        $console.error(params.url, response.status_code, response.error_message)
      end
    end
  end

  def handle_comment_submit(comment)
    comments = state.data
    # Optimistically set an id on the new comment. It will be replaced by an
    # id generated by the server. In a production application you would likely
    # not use Date.now() for this and would have a more robust system in place.
    comment['id'] = (Time.now.to_f * 1000).to_i

    new_comments = comments + [comment]
    state.data! new_comments
    HTTP.post(params.url, payload: comment) do |response|
      if response.ok?
        state.data! response.json
      else
        state.data! comments
        $console.error(params.url, response.status_code, response.error_message)
      end
    end
  end

  before_mount do
    state.data! []
  end

  after_mount do
    load_comments_from_server
    every(params.pollInterval) { loadCommentsFromServer }
  end

  def render
    div.commentBox do
      h1 { 'Comments' }
      CommentList(data: state.data)
      CommentForm(
        onCommentSubmit: proc { |comment| handle_comment_submit(comment) })
    end
  end
end

class CommentList < React::Component::Base
  param :data

  def render
    div.commentList do
      params.data.each do |comment|
        Comment(author: comment['author'], key: comment['id']) do
          comment['text']
        end
      end
    end
  end
end

class CommentForm < React::Component::Base
  param :onCommentSubmit, type: Proc

  before_mount do
    state.author! ''
    state.text! ''
  end

  def handle_author_change(e)
    state.author! e.target.value
  end

  def handle_text_change(e)
    state.text! e.target.value
  end

  def handle_submit(e)
    e.prevent_default
    author = state.author.strip
    text = state.text.strip
    return if text.empty? || author.empty?
    params.onCommentSubmit(author: author, text: text)
    state.author! ''
    state.text! ''
  end

  def render
    form.commentForm do
      input(type: 'text', placeholder: 'Your name', value: state.author)
        .on(:change) { |e| handle_author_change(e) }
      input(type: 'text', placeholder: 'Say something...', value: state.text)
        .on(:change) { |e| handle_text_change(e) }
      input(type: 'submit', value: 'Post')
    end.on(:submit) { |e| handle_submit(e) }
  end
end

Element['#content'].render do
  CommentBox(url: '/api/comments', pollInterval: 2000)
end
```


コレで完成です。あとは、好みのサーバを動かして、テストしてみて下さい。なお、server.rb は JSON をパースする処理が抜けているので、動きません。server.js あたりを試して下さい。

## 解説

Ruby 知っていたら解るよね？ そのうち、Opal の記事を書く(ような気がする)から許して。

## まとめ

いかがだったでしょうか？ Opal はまだ 0.9.2 (2016年3月6日現在) と 1.0 になっていなく、仕様が不安定な部分があります。React.rb も Opal 0.8.0 依存、ドキュメントの多数が工事中、React.js 0.14 相当に未対応など、まだまだ未完成です。しかし、十分実用的なレベルになっているかと思います。この altJS 戦国時代において、注目に値する組み合わせではないでしょうか？