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


今回はcoffee-reactを使わずにCoffeeScript **のみ** で書いてみたいと思います。JSX記法は使わずに、どれだけ綺麗に書けるようになるかが焦点かと思っています。

[React.jsチュートリアルをcoffee-reactで書いてみる](http://qiita.com/raccy/items/7c212f45cf8636e5927b)は見ておいてください。左記記事で説明済みの事については省いていきます。

## index.htmlを書き換える

`pubilc/index.html`を次のように書き換えます。

```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/react/15.0.1/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/remarkable/1.6.2/remarkable.min.js"></script>
    <script src="http://coffeescript.org/extras/coffee-script.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/coffeescript" src="scripts/example.coffee"></script>
    <script type="text/coffeescript">
      # 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>
```

coffee-react版との違いは、トランスパイラが`.../coffee-script.js`となって、MIME Typeが`text/coffeescript`、拡張子が`.coffee`となっているだけです。特に説明は不要ですね。

## example.jsをexample.coffeeに置き換える

`scripts/example.js`を下記の`scripts/example.coffee`に置き換えます。コードでやってることは全く一緒です。

```example.coffee
###
 * 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.
###

Comment = ({author, children}) ->
  rawMarkup = __html: (new Remarkable).render children.toString()
  React.DOM.div className: "comment",
    React.DOM.h2 className: "commentAuthor",
      author
    React.DOM.span dangerouslySetInnerHTML: rawMarkup

class CommentBox extends React.Component
  loadCommentsFromServer: =>
    $.ajax
      url: @props.url
      dataType: 'json'
      cache: false
      success: (data) =>
        @setState {data: data}
      error: (xhr, status, err) =>
        console.error @props.url, status, err.toString()

  handleCommentSubmit: (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 = Date.now()
    newComments = comments.concat [comment]
    @setState data: newComments
    $.ajax
      url: @props.url
      dataType: 'json'
      type: 'POST'
      data: comment
      success: (data) =>
        @setState {data: data}
      error: (xhr, status, err) =>
        @setState data: comments
        console.error @props.url, status, err.toString()

  constructor: ->
    super
    @state = data: []

  componentDidMount: ->
    @loadCommentsFromServer()
    setInterval @loadCommentsFromServer, @props.pollInterval

  render: ->
    React.DOM.div className: "commentBox",
      React.DOM.h1 "Comments"
      React.createElement CommentList, data: @state.data
      React.createElement CommentForm, onCommentSubmit: @handleCommentSubmit

CommentList = ({data}) ->
  React.DOM.div className: "commentList",
    data.map (comment) ->
      React.createElement Comment, author: comment.author, key: comment.id,
        comment.text

class CommentForm extends React.Component
  constructor: ->
    super
    @state = author: '', text: ''

  handleAuthorChange: (e) =>
    @setState author: e.target.value

  handleTextChange: (e) =>
    @setState text: e.target.value

  handleSubmit: (e) =>
    e.preventDefault()
    author = @state.author.trim()
    text = @state.text.trim()
    return if !text or !author
    @props.onCommentSubmit author: author, text: text
    @setState author: '', text: ''

  render: ->
    React.DOM.form className: "commentForm", onSubmit: @handleSubmit,
      React.DOM.input
        type: "text"
        placeholder: "Your name"
        value: @state.author
        onChange: @handleAuthorChange
      React.DOM.input
        type: "text"
        placeholder: "Say something..."
        value: @state.text
        onChange: @handleTextChange
      React.DOM.input type: "submit", value: "Post"

ReactDOM.render(
  React.createElement CommentBox, url: "/api/comments", pollInterval: 2000
  document.getElementById 'content'
)
```

## 解説

classにする、`=>`を使うとかはcoffee-reactと同じですが、変わったところを説明していきます。

### `React.createElement`と`React.DOM`

JSXを使わない場合、`React.createElement`や`React.DOM`等を使う必要があります。生のJavaScirptではとても冗長な書き方になってしまい、あまり効率的とは言えません。しかし、CoffeeScriptではJadeやSlimのようにスマートに書くことができます。少々書き方にコツがいりますが、JSX記法を使わなくても十分と言えるでしょう。

何度もReact...と書くのが嫌という場合は、[React.jsチュートリアルをLiveScriptで書いてみる](http://qiita.com/raccy/items/2eecd392944c164b188b)を参考にしてみてください。CoffeeScriptでもできないことはありません。

### ステートレス関数コンポーネント

状態を持たない`Comment`と`CommentList`は0.14系から使用できるようになった[ステートレス関数コンポーネント](https://facebook.github.io/react/docs/reusable-components.html#stateless-functions)にしてみました。今回は混ざってしまっているので綺麗とは言い難いですが、全体的にステートレスなコンポーネントだけで作成するのであれば、とても有効な記法です。とくにCoffeeScriptは最後の式を自動的にreturnするという特徴があるため、生のJavaScriptに比べて書きやすくなっています。

## まとめ

CoffeeScriptであればJSX記法を使わなくても大丈夫という感じです。とくに、`CommentList`で`data.map ...`の部分のようにそのまま書けることは利点です。JSX記法では{}内しかJavaScriptのコードとならないため、データの配列からElementの配列を作る場合は別途変数に入れる必要がありますが、JSX記法で無い場合は普通にその場で書いたものが引数と入れることができます。

ES6やTypeScriptではどうしてもJSX記法でないと書きにくいReactですが、CoffeeScriptであればそれも不要と言うことで、CoffeeScriptの良いところがよくわかる例の一つではないでしょうか？