WebpackerでReactを導入するときにつまづいたこと

エイチームライフスタイルWebエンジニアの@okonomiと申します。
[Ateam Lifestyle Advent Calendar 2017](https://qiita.com/advent-calendar/2017/ateam-lifestyle)、15日目をお送りします。

# はじめに

弊社ではRails製のWebサイトを複数運用しており、最近それらのサイトに徐々にWebpackerでReactを導入し始めています。
導入自体は他のエンジニアが進めているのですが、自分でも手順などを知っておきたくて試してみました。

webpacker gemを追加してHello Reactするところまでは楽勝でしたが、そこから実際にアプリを書き始めるとつまづくポイントがいくつかありました。
もしこれからWebpackerを導入しようというときに、何か参考になれば幸いです。

また、試行錯誤の結果をgithubに公開していますのでよろしければ合わせてご覧ください。
https://github.com/okonomi/rails-webpacker-sample

# 環境など

- macOS High Sierra
- Ruby 2.4.2
- Node.js v8.9.0
- Rails 5.1.2
- Webpacker 3.1.1

Webpackerは現在も活発に開発が進められています。
そのため、バージョンによって挙動が食い違う可能性があります！
本記事では執筆時点で最新の3.1.1を対象にしています。

# その1: FontAwesomeが読み込まれない

## 現象

コンポーネントの見栄えを良くしようとFontAwesomeを導入しようと思い、
[react-fontawesome](https://github.com/danawoodman/react-fontawesome)のサンプルコードどおりに記述したのですがフォントが表示されません…

```js:app/javascript/src/components/like_button.jsx
import React from 'react'
import FontAwesome from 'react-fontawesome'
import faStyles from 'font-awesome/css/font-awesome.css'

class LikeButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      likes: 0,
    };
  }

  render() {
    return (
      <button onClick={() => {this.setState({likes: this.state.likes +1})}}>
        <FontAwesome
          name="thumbs-up"
          cssModule={faStyles} // CSSを渡しているが効かない!
        /> {this.state.likes}
      </button>
    );
  }
}
```

![2017-12-13_03-10-16_re.png](https://qiita-image-store.s3.amazonaws.com/0/461/e9ba0f49-dea1-e4e8-8379-2e0eb0abb98f.png)

## 原因・対策

Webpackerのデフォルト設定ではCSS Modulesが有効になっていませんでした。

上記コードは`FontAwesome`コンポーネントに対してCSS Modulesでスタイルを指定していますが、CSS Modulesが有効になっていないため`faStyles`が`null`になってしまっていました。

CSS Modulesを有効にする方法はCHANGELOGに記載がありました。
https://github.com/rails/webpacker/blob/master/CHANGELOG.md#310---2017-12-11

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

// Enable css modules with sass loader
const sassLoader = environment.loaders.get('sass')
const cssLoader = sassLoader.use.find(loader => loader.loader === 'css-loader')

cssLoader.options = Object.assign(cssLoader.options, {
  modules: true,
  localIdentName: '[path][name]__[local]--[hash:base64:5]'
})

module.exports = environment
```

`environment`からCSS Loaderのオブジェクトを取り出してオプションを追加します。

`environment`はWebpackerにおけるwebpackの環境設定を統括するEnvironmentクラスのインスタンスで、実体はWebpackerの[package/environment.js](https://github.com/rails/webpacker/blob/v3.1.1/package/environment.js)で定義されています。
CSS Loaderのオプションは[package/rules/css.js](https://github.com/rails/webpacker/blob/v3.1.1/package/rules/css.js#L22)で指定されていますが、`modules`フラグが有効化されていないので追加します。これでCSS Modulesが有効になります！

CSS Loaderに指定できるオプションの一覧はこちらで確認できます。
https://github.com/webpack-contrib/css-loader#options

また、上記設定を加えるとCSS Modulesは`.scss` や `.sass` に対して有効になりますので、FontAwesomeのファイルも`.scss`に変更しておきます。

```diff:app/javascript/src/components/like_button.jsx
- import faStyles from 'font-awesome/css/font-awesome.css'
+ import faStyles from 'font-awesome/scss/font-awesome.scss'
```

あと最後に、viewに`stylesheet_pack_tag`を書いてCSSファイルが読み込まれるようにします。

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

![2017-12-13_22-51-17_re.png](https://qiita-image-store.s3.amazonaws.com/0/461/f0fbafd2-696c-d84d-386a-bd82791258b6.png)

## 補足

公式ドキュメントにもCSS Modulesを有効にする設定例が記載されています。
https://github.com/rails/webpacker/blob/master/docs/webpack.md#overriding-loader-options-in-webpack-3-for-css-modules-etc

こちらの設定もやっていることは同じで、違いは設定値の追加に使うのが`Object.assign`か[webpack-merge](https://github.com/survivejs/webpack-merge)かだけです。
今回の用途ではwebpack-mergeは大げさかなと思いますが、どちらにするかは好みで良さそうです。

# その2: turbolinks有効時にJSがうまく動かない

## 現象

Reactコンポーネントを設置したページを直接開くと問題ないのですが、トップページからReactコンポーネントを設置したページに遷移してくるとコンポーネントが起動しません。なぜ…

![2017-12-13 03.04.13.gif](https://qiita-image-store.s3.amazonaws.com/0/461/91f1ae15-85a5-0678-081e-1bc0a6fe92ec.gif)

## 原因・対策

ページ読み込み完了を契機にコンポーネントを起動していたのですが、listenするイベントに問題がありました。

```js:app/javascript/packs/application.js
document.addEventListener('DOMContentLoaded', () => {
  const elem = document.getElementById('like-button');
  if (elem) {
    ReactDOM.render(<LikeButton />, elem)
  }
})
```

turbolinksは御存知の通りページ遷移時にコンテンツの内容だけを入れ替えることでページ遷移を高速化しますが、そのため`DOMContentLoaded`イベントが発火しなくなります。
替わりにturbolinks側で用意されている`turbolinks:load`イベントをlistenすることでページ遷移がキャッチできます。

```diff:app/javascript/packs/application.js
- document.addEventListener('DOMContentLoaded', () => {
+ document.addEventListener('turbolinks:load', () => {
    const elem = document.getElementById('like-button');
    if (elem) {
      ReactDOM.render(<LikeButton />, elem)
    }
  })
```

turbolinks側で用意されているイベントはこちら。
https://github.com/turbolinks/turbolinks#full-list-of-events

また、`<body>`タグ内に`<script>`や`<style>`を置いてしまうとページ遷移時にJSが読まれてturbolinksの意味が薄れるので`<head>`に移動しました。

# その3: デプロイ時`assets:precompile`がなぜか失敗

## 現象

`bundle exec cap production deploy`でデプロイ実行すると、`assets:precompile`の段階でwebpackのCompiling…から進まなかったり

![2017-12-13_01-22-59_re.png](https://qiita-image-store.s3.amazonaws.com/0/461/4a589e07-b301-34b6-0add-5fefe21fab73.png)

エラー終了したりする現象が発生しました。

![2017-12-13_01-32-09_re.png](https://qiita-image-store.s3.amazonaws.com/0/461/d47cde07-9326-a87d-165c-08a8036d222e.png)

## 原因・対策

サーバのメモリ不足が原因でした。

デプロイ実行中にサーバ側のターミナルが「Cannot allocate memory」のようなメッセージを多発し始めたため、試しに`dstat`でデプロイ時のメモリの推移を見てみると空きメモリがみるみる減っていました。

![2017-12-13_01-36-33_re.png](https://qiita-image-store.s3.amazonaws.com/0/461/fe686871-12d6-8494-70c4-b43659444f3c.png)

今回はVagrantでデプロイ対象のサーバを立ち上げていたのですが、無設定だとメモリが512MBだったので、`Vagrantfile`を編集してメモリを1GBに増やしました。

```diff:Vagrantfile
  Vagrant.configure("2") do |config|
  # ...省略...

+ config.vm.provider "virtualbox" do |vb|
+   vb.memory = "1024"
+ end

  # ...省略...
  end
```

webpackのメモリ消費に関する情報はいくつか見かけるため、確証はないですが、webpackでビルドするときはサーバのメモリ容量に気をつけたほうがよさそうです。
また、公式ドキュメントにビルドパフォーマンスについてのページがあるので、ここを参考にすればメモリ使用量の改善ができるかもしれません。
https://webpack.js.org/guides/build-performance/
この辺り、詳しい方のご意見を伺いたいところです。

# おわりに

以上、WebpackerでReactを導入するときにつまづいたことをまとめてみました。
Webpackerはフロントエンド開発の色々なややこしいことを肩代わりしくれますが、連携して動いているミドルウェアやライブラリを理解していないと、一見不可解な挙動に振り回されてしまうことも分かりました。
挙動をひとつひとつ把握しつつ、React導入を進めていこうと思います。

明日は新卒デザイナーの@xrxoxcxoxさんがAdobe XDについて書いてくれるようです。よろしくお願いします！

---

[株式会社エイチームライフスタイル](http://life.a-tm.co.jp/)では、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
http://www.a-tm.co.jp/recruit/