Rails+Webpacker+Reactにbabel-plugin-react-css-modulesを入れる

スポンサーリンク

RailsのWebpacker+Reactでフロントエンドを作っているのですが、コンポーネントへのスタイルの当て方にも色々あるんですね。
今回はCSS Modulesという設計に基づいたbabel-plugin-react-css-modulesというパッケージを入れてCSSのローカルスコープ化をしたのですが、使い方がよくわからず

Warning: React does not recognize the `styleName` prop on a DOM element.

というWarningが出続けてかなりハマったのでメモを残します。

注意

自分はwebpack/Babelともにそんなに詳しくないので、以下の内容は一部必要のない設定や誤解もあるかもしれません。何か気になったらコメントで指摘頂けるとありがたいです。

その前に:Webpackerのキャッシュを消す方法

/public/packsディレクトリにはWebpackerが作成したファイルが大量に作成されています。これは最新のもの以外必要ないですし、消せば作り直されます。
プラグイン等の設定を変更しても開発サーバがファイルをキャッシュして反映されない場合があるので、設定変更したのに何も変わらないと思ったときはpacks以下を全部消してしまうといいです。

パッケージインストール

yarn add babel-plugin-react-css-modules

config/webpack/environment.js

CSS ModulesをWebpackerに入れる方法については公式ドキュメントに解説がありました。
webpacker/webpack.md at master · rails/webpacker
Webpackerでは素のWebpackの設定ファイルであるwebpack.config.jsではなくconfig/webpack/以下にあるjsを使うようです。environment.jsがベースの設定、test/development/productionはそれぞれの環境固有の設定だと思います。上のリンクにあるとおりenvironment.jsを編集します。

その前にwebpack-mergeを使ってるのでインストール。

yarn add webpack-merge

config/webpack/environment.js

const { environment } = require('@rails/webpacker')
const merge = require('webpack-merge')

const myCssLoaderOptions = {
  modules: true,
  sourceMap: true,
  localIdentName: '[path]___[name]__[local]___[hash:base64:5]'
}

const CSSLoader = environment.loaders.get('sass').use.find(el => el.loader === 'css-loader')

CSSLoader.options = merge(CSSLoader.options, myCssLoaderOptions)

module.exports = environment

ただここには2つの注意点がありました。

localIdentName

CSS ModulesではCSSのローカルスコープ化のため、クラス名が改変されます。その際HTML側のclass名とCSSのclass名の改変後のローカルスコープ名が一致しないとスタイルが当然反映されないのですが、Webpackerドキュメントの例にあるlocalIdentNameだとbabel-plugin-react-css-modulesの出力するDOM側のスコープ化クラス名と食い違いが発生していました。

現在のbabel-plugin-react-css-modulesのデフォルトに合わせるには

localIdentName: '[path]___[name]__[local]___[hash:base64:5]'

とする必要がありました。

babel-plugin-react-css-modulesのスコープ名のデフォルト形式は公式のOption一覧のgenerateScopedNameで確認できます。

CSS/SCSS

environment.loaders.get(‘sass’)の部分はCSSしか使わない場合(‘css’)に変える必要があるみたいです。SCSSを使う場合は’sass’。
ただどっちにしろ同じcss-loaderがインスタンス化されて使われるせいか、混在する場合片方書くだけで動いた何故か両方動いたのですが、順序などで問題が起こるかもしれません。よくわかりません。

Babel設定

PostCSSプラグイン追加

SCSSを使うには追加のPostCSSがSCSSを読み込むためのプラグインが必要みたいです。(後述:必要ないかも?)

yarn add postcss-scss

.babelrc

.babelrcにのpluginsにreact-css-modulesを追加します。

# 前半略
  "plugins": [
    "syntax-dynamic-import",
    "transform-object-rest-spread",
    [   
      "transform-class-properties",
      {   
        "spec": true
      }   
    ],  
    ["react-css-modules", {
      "filetypes": {
        ".scss": {
          "syntax": "postcss-scss"
        }
      }
    }]
  ]
}

こんな感じになります。配列なので追加する際1個上のプラグインにカンマを足すのを忘れずに。他のプラグインはプロジェクト作成初期から含まれてたものです。
SCSSを使わない場合はオプションを書く必要ありません。[“react-css-modules”]または”react-css-modules”で大丈夫です。

stylesheet_pack_tag

これも公式ドキュメントに書いてあるんですが、コンポーネントからのimportとは別にReactを使用するerb内でstylesheet_pack_tagでCSSを読み込む必要が何故かあります…。
ちなみにpublic/packs/manifest.jsonを見るとわかりますが、cssはどこに置いてもimportした側のjsと同じパスにマッピングされるようです。

例えば
javascript/packs/components/hoge.js
から相対パスで
javascript/src/hoge_style.css
をimportしていても、読み込み時は

<% stylesheet_pack_tag 'components/hoge' %>

となります。
スタイルをコンポーネントに対してローカル化するという思想上コンポーネントのパスになるということかと思います。

補足:postcss-scssは必要ないかも?

postcss-sccsはPostCSSにscssをパースさせるための構文プラグイン(実際にSCSSをCSSにコンパイルするわけではない)なんですが、これを入れずに.babelrcを

    ["react-css-modules", {
      "filetypes": {
        ".scss": {}
      }   
    }]  

としたところ普通にscss使えてるような気がします。filetypesは書かないとscss拡張子が反応されないので必要です。
なぜドキュメントに反してこれが大丈夫だと思ったかというと、webpackerはデフォルトでcss-loaderの前にscssに対してsass-loaderを通してるはずで、sass-loaderはSCSSをCSSにコンパイルしているはずなんです。なのでcss-loaderに渡った時点でSCSSはCSSになってると思います。
ただ最初に言ったとおりwebpackもBabelも入門中なので理解が正しいかはわかりません。

補足2:CSSが出力されないとき

importを行っているのにCSSが出力されないと思ったら、コンポーネントのwebpackコンパイル自体が失敗している(jsxのタグ不整合など)せいだったということもあったのでそれも確認してみてください。