ReactとDockerで既存のRailsアプリケーションをちょっとモダンに作り変える

# TL;DR
タイトルの通り

# はじめに
今年度に入ってからはReact Nativeでアプリを作っていたのですが、
7月からRailsで作られているサービスに携わることになったので、現在[Railsの勉強中](https://github.com/t-Asai/docker_rails)です。
いざ触って見ると、

- 環境構築が面倒
- バージョンが古いものが若干混じってる
- erbとかhamlとかなんか苦手
- Reactが書きたい
- コントローラがややこしい
- すぐに仕様は追加される
- 不必要にレンダリングされるのが嫌だ
- _formとかindexとか便利か？

などと~~ワガママや偏見を~~感じてきたので、
一旦、RailsをDockerに乗せ、[react-rails](https://github.com/t-Asai/awesome_rails)を使ってフロントをReactに書き変えていこうと思います。
(Docker化はワガママではなく、当初の予定通り)
将来的にはフロントは普通のReactに完全移行して、バックエンドも必要に応じて良しなに書き換えていきたいです。

# 目次
- Docker化
- Reactの準備
- CRUDの確認

# Docker化
(Dockerを使わない人は、多分飛ばして大丈夫です。)
元々は普通のrubyのimageを使っていたのですが、imageサイズが1gbを超えてしまったので
なんとなく小さくしようと思い、imageはruby:2.5.1-alpineを使いました。(会社のやつはrubyのままです)
alpineにrubyを入れた方が小さくできるのかもしれませんが、
270mbまで落ち着いたので、まぁ良さそうです。

```Dockerfile
FROM ruby:2.5.1-alpine

WORKDIR /awesome_app
COPY Gemfile* /awesome_app/

RUN apk upgrade --no-cache && \
  apk add --update --no-cache \
  postgresql-client \
  nodejs \
  yarn \
  tzdata && \
  apk add --update --no-cache --virtual=build-dependencies \
  build-base \
  linux-headers \
  libxml2-dev \
  libxslt-dev \
  postgresql-dev \
  ruby-dev \
  yaml-dev \
  zlib-dev && \
  gem install bundler && \
  bundle install -j4 && \
  apk del build-dependencies --purge && \
  rm -rf /tmp/src && \
  rm -rf /var/cache/apk/

RUN npm install yarn -g && yarn install && gem update bundler && bundle install;
```
alpineはbash等、色々と足りてない様ですが、慣れればなんとかなると思います。
足りないものがあれば、apkで適宜追加して凌ぎます。

docker-compose.yamlは下のような感じになりました。

```docker-compose.yaml
version: '3.5'

services:
  postgres:
    image: postgres:10.4
    environment:
      TZ: Asia/Tokyo
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - 5432:5432
    networks:
      - docker-network
    volumes:
      - postgres-data:/var/lib/postgresql/data
    stop_signal: SIGINT
    restart: always
    
  webpacker:
    build: '../awesome_app'
    command: ash -c 'rm -rf /awesome_app/public/packs; /awesome_app/bin/webpack-dev-server'
    volumes:
      - ../awesome_app:/awesome_app
    ports:
      - 3035:3035
    networks:
      - docker-network
    restart: always

  web:
    build: '../awesome_app'
    command: ash -c 'rm /awesome_app/tmp/pids/server.pid; bundle exec rails s'
    image: rails:dev
    volumes:
      - ../awesome_app:/awesome_app
    environment:
      TZ: Asia/Tokyo
    networks:
      - docker-network
    ports:
      - 3000:3000
    depends_on:
      - webpacker
      - postgres
    restart: always

networks:
  docker-network:
volumes:
  postgres-data:
```

開発環境ではwebpack-dev-serverを立てて、Reactの部分を表示させます。
Dockerを使わない場合は、デフォルトのままで良いのですが、今回はDockerを使うことにしたので
config/environments/develop.rbとconfig/webpacker.ymlを編集する必要があります。

```develop.rb
  config.x.webpacker[:dev_server_host] = "http://webpacker:3035"
```
```webpacker.yml
  dev_server:
    https: false
    host: webpacker
    port: 3035
    public: 0.0.0.0:3035
    hmr: true
    # Inline should be set to true if using HMR
    inline: true
    overlay: true
    compress: true
    disable_host_check: true
    use_local_ip: false
    quiet: false
    headers:
      'Access-Control-Allow-Origin': '*'
    watch_options:
      ignored: /node_modules/
```
これでDocker環境は完成です。

# Reactの準備
次に、Reactの準備です。[react-rails](https://github.com/reactjs/react-rails)と[webpacker](https://github.com/rails/webpacker)を使っていきます。
インストール方法などは以下の記事が参考になりました。
- https://qiita.com/kouheiszk/items/c85e70e331ba75841818
- https://qiita.com/joe-re/items/96f12dda4a62470d1d7c
react-railsとwebpackerは、デフォルトではapp/javascript/packs以下のファイルを参照しているようです。
webpackerに関しては、config/webpacker.ymlで設定を変更できるのですが、
react-railsの方がどこで設定を上書きすれば良いのか分からなかったので、そのままにしています。
そして、app/javascript/packs/application.jsを

```app/javascript/packs/application.js
// By default, this pack is loaded for server-side rendering.
// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
var componentRequireContext = require.context("components", true)
var ReactRailsUJS = require("react_ujs")
ReactRailsUJS.useContext(componentRequireContext)
```
と書き換えることで、app/javascript/components/Users.tsxを.erbから

```erb
<%= react_component('Users') %>
```
の様に呼べる様です。
良くやる様に、

```bash
app/javascript/components/Users/
└── index.tsx
```
とした場合、index.tsxをちゃんと読んでくれるので、
適当にパーツごとに分けて開発を進めることも出来そうです。

Reactを書くとなると、ステートレスに書きたくなりますが、
react-railsではステートフルに書く方が簡単そうです。
ReactとRailsを完全に分離できたら、Reduxを入れつつステートレスに書き換えていけば良いかと思います。

これで、Rails特有の_form, index, newなどのファイルが不要に出来、
また、機能が全てReact側に移れば、erbファイルに記述するものも1行で済みます。

```bash
users/
└── index.html.erb
```
```Users.erb
<%= react_component('Users') %>
```
フォーム登録などは別ページで行った方が良い場合もあると思うので、
その場合はerbを複数用意したまま、それぞれをReactに移していけば良いかと思います。


# CRUDの確認
最後にReactからaxiosでCRUDをする方法を書いていきます。
Railsはjsonを返すapiサーバとして使える様で、コントローラ側はあまり苦労はしないかと思います。
また、axiosでpost, get, put, deleteするだけなのでフロント側もそこまで苦労はしなさそうです。
CSRFの設定は、このままだと少し引っかかるので、そこだけ書いておきます。
デフォルトではRailsの方でCSRF対策をしてくれているので、
React側からaxiosでReadは出来てもCreateなどは出来ません。
特にセキュリティなどには気を使わなくていいアプリケーションであれば、application_controller.rbに

```application_controller.rb
protect_from_forgery with: :null_session
```
と追記すれば動いてくれる様です。
今回は、[react-on-rails](https://github.com/shakacode/react_on_rails)を使ってaxiosのheaderにCSRF用のトークンを埋め込みます。
(最初に見たときはreact-railsとreact-on-railsのどちらも大差なく感じたので
githubの星が多いreact-railsを使っていたのですが
今後もこの手の細かいところで差が出てくるかもしれません)

```tsx
import axios from "axios";
//@ts-ignore
import ReactOnRails from "react-on-rails";

axios.defaults.withCredentials = true;

axios.interceptors.request.use(
  config => {
    config.headers["X-CSRF-TOKEN"] = ReactOnRails.authenticityToken();
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

```
これで、DBへのCRUDもできる様になりました。

# 終わり
これで少しずつerbをReactに入れ替えていく事が出来るハズです。
(viewのファイルの数に比例して手間ですが。。。)
ゼロから作るならRailsとReactは別で作った方が圧倒的に作りやすいかと思いますが、
既存のアプリケーションを移行するときは、この様な形で少しずつ行う方が安全かと思います。

流石に一ヶ月もRailsを触っていると、ある程度慣れてきたので、hamlでもそこまで困らなくなってきたのですが
仕様変更・機能追加に柔軟に対応しようとすると、フロントである程度綺麗に整形したものを
Railsに渡す方が開発しやすそうなので、ちょっと頑張ります。

# TODO
- nested_formなどRails特有の諸々が使えるのかを調べる
- セッション等の取り扱いを調べる
- 今のサービスのボトルネックになっているものはデータベースの様なので、そっちも調べる