CoffeeScript
reactjs
React
CoffeeScript2

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

Reactのドキュメントではすでにこのチュートリアルは使用されていないのですが、そこら辺は気にせずに行きます。

CoffeeScript 2とは?

CoffeeScriptのメジャーアップデートです。2017年9月12日現在はbeta5ですが、期待の新人です。

v1.xから大きく変わったことは、コンパイル後のコードがES2015+になった事です。class等はclass等のまま出力されます。また、JSX表記がサポートされました。ただし、JSXをJavaScriptに変換するのでは無く、これまたJSXはJSXのまま出力されますので、別途Babel等での変換が必要です。

チュートリアルの書き換え

準備

いつものように https://github.com/reactjs/react-tutorial をcloneします。

今回はwebpackで一つにまとめたいと思います。理由としては、import文を使いたかったとか、JSX表記でbabelを使う必要があるとか、色々です。ということで必要なモジュール類を入れていきます。各コマンドはyarnを使ってますので、yarnは予め入れておいてください。

yarn add react react-dom jquery remarkable
yarn add webpack babel-loader babel-core babel-preset-env babel-preset-react -D
yarn add coffeescript@next coffee-loader -D

coffee-loaderはCoffeeScript 2に対応済みです。

ファイルの用意

まずはwebpackの設定ファイルを用意します。せっかくなので、CoffeeScriptにしました。

webpack.config.coffee
path = require('path')

module.exports =
  entry: './src/example.coffee'
  output:
    filename: 'example.js'
    path: path.resolve(__dirname, 'public/scripts')
  module:
    rules: [
      {
        test: /\.coffee$/,
        use: [
          {
            loader: 'babel-loader'
            options:
              presets: ['env', 'react']
          }
          'coffee-loader'
        ]
      }
    ]

loaderの順番を変えるとうまくいきませんのでご注意ください。

次にindex.htmlの書き換えです。webpackで一つにまとめるため、読み込むJavaScriptのたった一つになります。

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" />
  </head>
  <body>
    <div id="content"></div>
    <script src="scripts/example.js"></script>
  </body>
</html>

最後はCoffeeScriptのメインコードです。srcにディレクトリを作って、そちらに置くことにします。

src/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.
####

import React from 'react'
import ReactDOM from 'react-dom'
import $ from 'jquery'
import Remarkable from 'remarkable'

class Comment extends React.Component
  rawMarkup: ->
    md = new Remarkable
    rawMarkup = md.render(@props.children.toString())
    { __html: rawMarkup }

  render: ->
    <div className="comment">
      <h2 className="commentAuthor">
        {@props.author}
      </h2>
      <span dangerouslySetInnerHTML={@rawMarkup()} />
    </div>

class CommentBox extends React.Component
  constructor: (props) ->
    super props
    @state = data: []

  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()

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

  render: ->
    <div className="commentBox">
      <h1>Comments</h1>
      <CommentList data={@state.data} />
      <CommentForm onCommentSubmit={@handleCommentSubmit} />
    </div>

CommentList = ({data}) ->
  commentNodes = data.map (comment) ->
    <Comment author={comment.author} key={comment.id}>
      {comment.text}
    </Comment>
  <div className="commentList">
    {commentNodes}
  </div>

class CommentForm extends React.Component
  constructor: (props) ->
    super props
    @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: ->
    <form className="commentForm" onSubmit={@handleSubmit}>
      <input
        type="text"
        placeholder="Your name"
        value={@state.author}
        onChange={@handleAuthorChange}
      />
      <input
        type="text"
        placeholder="Say something..."
        value={@state.text}
        onChange={@handleTextChange}
      />
      <input type="submit" value="Post" />
    </form>

ReactDOM.render(
  <CommentBox url="/api/comments" pollInterval={2000} />,
  document.getElementById 'content'
)

コンパイル

後はコンパイルするだけです。

yarn run webpack

CoffeeScript -> ES2015+ with JSX -> JavaScript と変換されたexample.jsが生成されます。

解説

見ればわかるとおり、coffee-reactで書いたコードとほぼ同じです。coffee-reactで対応していた部分がCoffeeScript自体でJSXを認識し、Babelでさらに変換できるようになっただけとも言えます。

違いは下記の2点だけです。

  • モジュールベース
    webpackでまとめるためにモジュールベースにしています。つまりimport文を使っていると言うことです。
  • superの動作
    CoffeeScript 2からsuperとだけ書くことはできなくなりました。そのため、constructor()ではpropsを引数に取り、super(props)と渡す必要があります。

まとめ

いかがだったでしょうか?事例がほとんど見つからなくて、webpackでCoffeeScript + Babelの動作をさせるのに少し苦労しましたが、あとはいつも通りでした。JSXが本家で対応、そして、モジュールベースにできるようになった分、やりやすくなったのでは無いかと思っています。かといってCoffeeScriptに未来があるかどうかはまた別の問題かも知れませんが。