Rails 5.1 API mode + webpacker + react + reactstrap で ToDO アプリを書く

# モチベーション

* Rails 5.1 がリリースされて webpacker が入りました
    * http://edgeguides.rubyonrails.org/5_1_release_notes.html
* 5.1 もある程度リリースが重なってきたので、まともに使えると思うので使ってみたい
    * 2018/4/4 時点で 5.1.6 が最新 ( https://github.com/rails/rails/releases )
* ついでに API mode も試せるといいなぁ

というわけで、1時間くらいで試した記録を残しておくことにしました。

# 追加記事

* 今回の実装に JWT 認証を追加した記事を書きました
* [Rails 5.1 API mode + webpacker + react + reactstrap な ToDO アプリに認証機能を追加する (sorcery gem で JWT)](https://qiita.com/kaishuu0123/items/27ba55e774ac1b7a94fc)

# 対象読者

* Rails は触った事があるが、Rails 5.1 の webpacker は触ったことがない人
* フロントエンド技術を(軽く)触ってみたい人
    * コードサンプルの意味を知るにはある程度 React やっておいた方がスムーズかもしれません
    * 意味分からなくても後で調べればきっと分かるよ!

# 作業場所(Github)

* https://github.com/kaishuu0123/rails5.1-react-reactstrap-example

# 実行環境

* Mac OS X (もしくは Linux でも)
* Ruby 2.5.1
    * rbenv や rvm などでセットアップが終わっている前提とします
    * bundler はインストール済み (`gem install bundler`)

# デモ
* こんな感じのものを作ります
![rails5.1_and_react_reactstrap.gif](https://qiita-image-store.s3.amazonaws.com/0/19717/6544ff03-b149-75e1-868d-34e5e2b9787f.gif)

* 機能
    * タスクの作成
    * タスクの削除
* UI は Bootstrap 4 + reactstrap を利用
* デモ上だと分かりづらいですが、 React で SPA っぽく動いています
* `POST` メソッドを Rails 側に送信してタスク作成
* 削除時には `DELETE` メソッドでタスクの削除を実現しています

# プロジェクトの準備
## Rails をインストール
プロジェクトディレクトリの作成 & `bundle init` でGemfile の作成

```shell
$ mkdir rails_react_tutorial
$ cd rails_react_tutorial
$ bundle init
```

Gemfile に `gem "rails", "~> 5.1.0"` を追記

```diff
+gem 'rails', '~> 5.1.0'
```

Rails を vendor/bundle ディレクトリ以下にインストール

```shell
$ bundle install --path=vendor/bundle
```

## プロジェクトの初期化
インストールが完了したら、`bundle exec rails new` コマンドでプロジェクトの初期化をします

```shell
$ bundle exec rails new . --webpack=react --api 
```

各オプションの意図

* `--api`
    * Rails サーバ側自体は API モードで利用する
* `--webpack=react`
    * React を利用するので `webpack=react` を指定
    * angular　や vue.js なども指定できます

コマンド実行時に以下のような出力がされますが、上書きしても問題ないので Y で進めます

```shell
[XXXX@XXXXX:~/work/rails_react_tutorial]$ bundle exec rails new . --webpack=react --api
       exist  
      create  README.md
      create  Rakefile
      create  config.ru
      create  .gitignore
    conflict  Gemfile
Overwrite /Users/XXXXX/work/rails_react_tutorial/Gemfile? (enter "h" for help) [Ynaqdh] Y
```

インストールまで完了すると `rails` コマンドなどが使えるようになっていると思います
興味がある方は `./bin/rake -T` などをしてタスク一覧を見てみると面白いと思います

## React アプリを Rails 側で配信する環境を整える

* Rails 側は API mode で作成しているため、デフォルトでは HTML を配信しません
* React アプリを配信するためのページが必要なので、セットアップを行っていきます

Controllerを作成

```shell
$ ./bin/rails g controller Home index
```

`config/routes.rb` に `home#index`　が root になるように設定

```diff:config/routes.rb
 Rails.application.routes.draw do
+  root 'home#index'
   get 'home/index'
```

HomeController を以下のように変更する

```diff:app/controllers/home_controller.rb
-class HomeController < ApplicationController
+class HomeController < ActionController::Base
+  include ActionController::RequestForgeryProtection
+  include ActionController::ImplicitRender
+  include ActionView::Layouts
+
+  layout "application"
+
   def index
   end
 end
```

* ApplicationController を継承すると JSON を返してしまうので `ActionController::Base` を継承させます
* 通常の ActionView を HomeController 内のみで動かすために `include ActionView::Layouts` などを追記しています

`app/views/layouts/application.html.erb` を作成

```html:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
    <title>SampleView</title>
    <%= csrf_meta_tags %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
```

`app/views/home/index.html.erb` を以下の内容で作成します

```shell
# まずはディレクトリを作っておく
$ mkdir app/views/home
```

```html:app/views/home/index.html.erb
<!-- この内容で app/views/home/index.html.erb を作成 -->
<%= javascript_pack_tag 'hello_react' %>
```

* hello_react というファイル名が出てきましたが、これは `app/javascript/packs` 以下にあります
* React でコンポーネントを作成する場合には以後 `app/javascript/packs` に作成していきます

## 無事セットアップできたか確認する

Rails 側を起動します

```shell
$ ./bin/rails s
```

webpack-dev-server の起動が必要なため、別ターミナルなどで起動します

```shell
$ ./bin/webpack-dev-server
```

`http://localhost:3000` にブラウザでアクセスして `Hello React!` と表示されれば、成功です

# アプリを作る

* やっと本筋に来ました
* まずは API 側で CRUD ができるエンドポイントを作って、その後にフロントエンド(React)側を作り込んでいきます

## Rails API(バックエンド)側の実装

モデルを作成

```shell
# Task モデルを作成
$ ./bin/rails g model task title:string description:string

# migrate して Task を DB に反映する
$ ./bin/rails db:migrate
```

コントローラを作成

```shell
$ ./bin/rails g controller Api::V1::Tasks
Running via Spring preloader in process 15338
      create  app/controllers/api/v1/tasks_controller.rb
```

`app/controllers/api/v1/tasks_controller.rb` に CRUD 用のアクションを一気に定義します

```ruby:app/controllers/api/v1/tasks_controller.rb
class Api::V1::TasksController < ApplicationController
  before_action :set_task, only: [
    :show, :update, :destroy
  ]

  def index
    render json: Task.order(created_at: :desc).all
  end

  def create
    @task = Task.create!(task_params)
    render json: @task
  end

  def show
    render json: @task
  end

  def update
    @task.update(task_params)
    render json: @task
  end

  def destroy
    @task.destroy
    head :no_content
  end

  private

  def task_params
    params.permit(:title, :description)
  end

  def set_task
    @task = Task.find(params[:id])
  end
end
```

`TasksController` にエンドポイントとなるアクションを一通り作ったので、 `config/routes.rb` に `resources :tasks` を追記します

```ruby:config/routes.rb
  namespace :api, {format: 'json'} do
    namespace :v1 do
      resources :tasks
    end
  end
```

API からデータを取得できるように `rails console` でデータを登録しておきます

```bash
$ ./bin/rails c
Running via Spring preloader in process 17913
Loading development environment (Rails 5.1.6)
irb(main):001:0> Task.create(title: "foo", description: "bar")
```

`curl http://localhost/api/v1/tasks` をして作成したデータが返ってくれば OK です
また、 Task エントリの作成は以下のコマンドで確認できます

```shell
curl -H 'Content-Type:application/json' \
 -d '{"title":"title1","description":"desc1"}' \
 http://localhost:3000/api/v1/tasks
```

## React アプリ(フロントエンド側)の実装

* 見た目は無骨になりますが、Bootstrap の適用前に骨組みを作っていきます
* まずはディレクトリ構成を決めます
    * ここでは最初に完成形の構造を晒してしまいます
* `app/javascript` 以下に `components` ディレクトリを作成し、React のコンポーネントを格納するようにし、 `application.js` では `app.jsx` を import するようにします

```
├── components
│   ├── app.jsx
│   ├── header.jsx
│   ├── task-form.jsx
│   ├── task-row.jsx
│   └── task-table.jsx
└── packs
    └── application.js
```

まずは、 `app/views/home/index.html.erb` を以下のように修正します

```html:app/views/home/index.html.erb
<div id='example-app'></div>
<%= javascript_pack_tag 'application' %>
```

上記の `<div id=...` の箇所が React アプリケーションがレンダリングされる場所です。
補足ですが、`<%= javascript_pack_tag ...`の箇所は、前述では `hello_react` を試しましたが、React の最初の起点は `app/javascript/packs/application.js` としたいので、`application` にしています。

### 各コンポーネントのコードと解説

* ここではコードの例を示しつつ、インラインでコメントをしたほうが分かりやすいかな、と思ったので一気にコードを貼り付けています

```jsx:app/javascript/packs/application.js
console.log('Hello World from Webpacker')

import React from 'react';
import ReactDOM from 'react-dom';
import App from '../components/app';

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <App />, // 独自に記述した App クラス (../components/app.jsx)を指定
    document.getElementById('example-app')  // example-app をマウントポイントにする
  )
})
```

```jsx:app/javascript/components/app.jsx
import React from 'react';
import Header from './header';
import TaskForm from './task-form';
import TaskTable from './task-table';

class App extends React.Component {
  constructor(props) {
    super(props);

    // タスク一覧を格納する配列を state として初期化
    this.state = {
      tasks: [],
    }

    // タスクを取得するメソッドを this に bind
    this.getTasks = this.getTasks.bind(this);
  }

  componentDidMount() {
    // コンポーネントマウント時にタスク一覧を取得する
    this.getTasks()
  }

  getTasks() {
    // Rails 側の /api/v1/tasks に GET リクエストを送ってタスク一覧を取得
    let request = new Request('/api/v1/tasks', {
      method: 'GET',
      headers: new Headers({
        'Content-Type': 'application/json'
      })
    });

    fetch(request).then(function (response) {
      return response.json();
    }).then(function (tasks) {
      // 取得が完了したら state にセットする
      this.setState({
        tasks: tasks
      });
    }.bind(this)).catch(function (error) {
      console.error(error);
    });
  }

  render() {
    const { tasks } = this.state;

    return (
      <div>
        <Header title='Rails 5.1 + webpacker + React + Reactstrap Example' />
        <div>
          {/*
            * TaskForm コンポーネント起因でタスクを作成した際に
            * タスク一覧を再取得するために
            * getTasks メソッドを props として渡す
            */}
          <TaskForm getTasks={this.getTasks} />
          {/*
            * TaskRow (TaskTable の中) コンポーネント起因でタスクを削除した際に
            * タスク一覧を再取得するために
            * getTasks メソッドを props として渡す
            * tasks はタスク一覧
            */}
          <TaskTable tasks={tasks} getTasks={this.getTasks} />
        </div>
      </div>
    )
  }
}

export default App;
```

```jsx:app/javascript/components/header.jsx
import React from 'react';

class Header extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    // <Header title=' ... で渡された値を表示
    return (
      <div>
        <h1>{this.props.title}</h1>
      </div>
    )
  }
}

export default Header;
```

```jsx:app/javascript/components/task-form.jsx
import React from 'react';

class TaskForm extends React.Component {
  constructor(props) {
    super(props)

    // 入力フォーム(input) 用の state をセットしておく
    this.state = {
      title: '',
      description: ''
    }
    
    //　タスクを作成するメソッドを this に bind する
    this.createTask = this.createTask.bind(this);
  }

  createTask(event) {
    // Rails 側の /api/v1/tasks を POST メソッドで叩き、タスクを作成する
    let request = new Request('/api/v1/tasks', {
      method: 'POST',
      headers: new Headers({
        'Content-Type': 'application/json'
      }),
      body: JSON.stringify({
        title: this.state.title,
        description: this.state.description
      })
    });

    fetch(request).then(function (response) {
      return response.json();
    }).then((task) => {
      // タスク作成が成功したら、タスク一覧を再取得する
      this.props.getTasks();
    }).catch(function (error) {
      console.error(error);
    }).finally(() => {
      this.setState({
        title: '',
        description: ''
      })
    });

    // preventDefault でブラウザ起因の onSubmit イベントを打ち消す
    // この記述が無いとページが遷移してしまう
    event.preventDefault();
  }

  render() {
    let { title, description } = this.state;

    return (
      <form onSubmit={this.createTask}>
        <label>Title</label>
        <input
          type="text" value={title}
          placeholder="Title"
          onChange={(e) => {
            this.setState({
              title: e.target.value
            })
          }}
        />
        <label className="mr-sm-2">Description</label>
        <input
          type="text" value={description}
          placeholder="Description"
          onChange={(e) => {
            this.setState({
              description: e.target.value
            })
          }}
        />
        <input type="submit" value="Create Task" />
      </form>
    )
  }
}

export default TaskForm;
```

```jsx:app/javascript/components/task-table.jsx
import React from 'react';
import TaskRow from './task-row'

class TaskTable extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    const { tasks, getTasks } = this.props;

    // 渡された tasks を map で回し、TaskRow コンポーネントとしてまとめてレンダリング
    return (
      <table>
        <thead>
          <tr>
            <th>Title</th>
            <th>Description</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {tasks.map(function (task, index) {
            return (
              <TaskRow
                key={index}
                id={task.id}
                title={task.title}
                description={task.description}
                getTasks={getTasks}
              />);
          }.bind(this))}
        </tbody>
      </table>
    );
  }
}

export default TaskTable;
```

```jsx:app/javascript/components/task-row.jsx
import React from 'react';

class TaskRow extends React.Component {
  constructor(props) {
    super(props);

    this.deleteTask = this.deleteTask.bind(this);
  }

  deleteTask(id) {
    // Rails 側の /api/v1/tasks/{taskID} を DELETE メソッドで叩き、Task の削除を実行する
    let request = new Request(`/api/v1/tasks/${this.props.id}`, {
      method: 'DELETE',
      headers: new Headers({
        'Content-Type': 'application/json'
      }),
    });

    fetch(request).then(function (response) {
      return response;
    }).then(() => {
      // DELETE 完了後に再度タスク一覧を取得
      this.props.getTasks();
    }).catch(function (error) {
      console.error(error);
    });
  }

  render() {
    return (
      <tr>
        <td>{this.props.title}</td>
        <td>{this.props.description}</td>
        <td>
          <a href="#" onClick={() => this.deleteTask(this.props.id)}>Delete</a>
        </td>
      </tr>
    )
  }
}

export default TaskRow;
```

# reactstrap + bootstrap 4 導入

`yarn` で `reactstrap` と `bootstrap` をインストールします

```shell
$ bundle exec yarn add reactstrap bootstrap@4.0.0
```

bootstrap を　import するために、 `app/javascript/packs/` 以下に `bootstrap.scss` を作成します

```sass:app/javascript/packs/bootstrap.scss
@import '~bootstrap/dist/css/bootstrap';
```

筆者はサクッと導入できると思っていましたが、ここで一工夫必要になります。
bootstrap 4 系は(まだ?) jQuery やその他の JS に依存しているため、 `config/webpack/environment.js` を以下のようにする必要があります

```js:config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

environment.plugins.append('Provide', new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    Popper: ['popper.js', 'default']
}))

module.exports = environment
```

最後に `app/views/layouts/application.html.erb` に以下のタグを追記します

```diff:app/views/layouts/application.html.erb
+    <%= stylesheet_pack_tag 'bootstrap' %>
```

これで Rails 側から bootstrap が使えるようになりました。
上手く行っていれば、 React 側のタグに `className` などで bootstrap 4 のクラスを書くと適用されると思います。

適用例として https://github.com/kaishuu0123/rails5.1-react-reactstrap-example にコードを配置しましたので、参考になれば幸いです

# 所感

* 意外と手軽にフロントエンド開発をすることができて驚いています
    * 一人プロジェクトで使うには十分実用的
    * コマンドが多いのでメモを残すのは結構辛かった(´-﹏-`；)
        * けど、実際にやってみると意外とサックリできると思います
* 考えられる欠点
    * フロントエンドの技術が Rails プロジェクト内に混ざるので babel や webpack etc ... を理解していないと使うのは難しそう
    * チーム開発になるとフロントエンドとバックエンドは分けて考えた方がメリットある ... のかなぁ？
    * Turbolinks と連携するとなると辛そう

## 他にも色々試してみたいこと
* Redux や React Router 導入
* Sorcery gem を使って jwt 認証
* React 以外も試してみたい (Angular, Vue.js)
* 本番デプロイ方法の検討 (asset:precompile との連携など)
* 今回は Rails 側に HTML の配布まで実装したが webpack-dev-server がせっかくあるのでもっと独立して フロントエンド/バックエンドで開発が分けられるか調べてみる