経緯
これまで webpack-dev-server の Hot Module Replacement を React/Redux でも有効にするために babel-preset-react-hmre を使っていたが、いつの間にか GitHub のリポジトリがなくなっていて deprecated ぽいので別のプラグインを探していた。
公式ドキュメント を読む限り React Hot Loader が一番有力のようなので、そちらへ移行する。
サンプルコード(GitHub)
先に、今回の移行後のコードが確認できる GitHub リポジトリを載せておく。
設定手順
ここを読む。
Getting Started · React Hot Loader
※以下、(Step X) は上記ドキュメント中の Step と対応している
React Hot Loader のインストール
2017/08/09 時点で v3 はベータ版のようなので、 @next
をつけてインストールする。
$ npm install --save-dev react-hot-loader@next
webpack 側の設定をする (Step1)
ここについては webpack-dev-server をどのように利用しているかによってやることが異なる。
(Option 1) webpack-dev-server をスタンドアロンなサーバーとしてそのまま利用している場合
これまで通り --hot
オプションつきで実行すれば OK。
// package.json "scripts": { "start": "webpack-dev-server --hot" },
(Option 2) webpack-dev-server をカスタマイズして使っている場合
未検証。ドキュメントの通りにやればいいのでは。
(Option 3) の webpackDevMiddleware, webpackHotMiddleware として Express に組み込んで使っている場合
自分の場合はこれ。
参考:webpack-dev-serverをExpressに組み込んで使う(webpack-dev-middleware, webpack-hot-middleware) - dackdive's blog
ドキュメントは TODO になっていたが、以下ができていれば特に設定変更は不要だった。
- webpack.config.js の
entry
に'webpack-hot-middleware/client'
を追加する - webpack.config.js の
plugins
にnew webpack.HotModuleReplacementPlugin()
を追加する - Express 側で以下のように middleware を組み込む
import express from 'express'; import path from 'path'; const app = express(); const port = process.env.PORT || 8080; if (process.env.NODE_ENV !== 'production') { /* eslint-disable global-require, import/no-extraneous-dependencies */ const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackHotMiddleware = require('webpack-hot-middleware'); const config = require('./webpack.config.dev.babel').default; const compiler = webpack(config); app.use(webpackHotMiddleware(compiler)); app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath, })); } // Serve static files app.use(express.static(path.join(__dirname, 'public'))); console.log(`Served: http://localhost:${port}`); app.listen(port, (err) => { if (err) { console.error(err); } else { console.info(`==> 🌎 Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`); } });
詳細は リポジトリ を参照。
Babel または webpack に react-hot-loader を追加する (Step 3 の 2)
2 通りの方法がある。
.babelrc
に
{ "plugins": [ "react-hot-loader/babel" ] }
を追加するか、または webpack.config.js 側に
module: { loaders: [{ test: /\.js$/, loaders: ['react-hot-loader/webpack', 'babel'], include: path.join(__dirname, 'src') }] }
を追加する。
いずれも、 NODE_ENV === development
のときだけ、とは書かれていなかった。
アプリのエントリーポイントに react-hot-loader/patch
を追加する (Step 3 の 3)
こちらも 2 通りの方法がある。
アプリのルートコンポーネントとなる JS ファイルの1行目に
import 'react-hot-loader/patch';
を追加するか、または webpack.config.js の entry
に
entry: [ 'react-hot-loader/patch', // これを追加 './scripts/index' // アプリのエントリーポイント ]
を追加する。
自分は前者の方にした。
ルートコンポーネントに HMR の仕組みを入れる (Step 2)
最後に、アプリのルートコンポーネントに HMR の仕組みを入れる。
https://github.com/zaki-yama/redux-express-template/blob/master/public/index.js
import 'react-hot-loader/patch'; import React from 'react'; import { Provider } from 'react-redux'; import { render } from 'react-dom'; import { AppContainer } from 'react-hot-loader'; import configureStore from './store/configureStore'; import App from './containers/App'; import './styles/index.scss'; const store = configureStore(); const rootEl = document.getElementById('root'); render( <AppContainer> <Provider store={store}> <App /> </Provider> </AppContainer>, rootEl, ); if (module.hot) { module.hot.accept('./containers/App', () => { render( <AppContainer> <Provider store={store}> <App /> </Provider> </AppContainer>, rootEl, ); }); }
babel-preset-react-hmre のときはこのあたり自前で実装する必要はほぼなかったんだけど、React Hot Loader だと if (module.hot) { ... }
部分の処理が必要になるらしい。
<Provider store={store}> <App /> </Provider>
部分は Redux を使っているときのお決まりの書き方だが、これをさらに React Hot Loader が提供している <AppContainer />
というコンポーネントでラップする。
余談:ハマったポイント
ドキュメントでは
const render = Component => { ReactDOM.render( <AppContainer> <Component /> </AppContainer>, document.getElementById('root') ); } render(RootContainer); if (module.hot) { module.hot.accept('./containers/rootContainer', () => { render(RootContainer) }); }
というように記載されていたので、最初は以下のようにした。
// index.js import 'react-hot-loader/patch'; import React from 'react'; import { render } from 'react-dom'; import { AppContainer } from 'react-hot-loader'; import Root from './containers/Root'; const renderApp = (Component) => { render( <AppContainer> <Component /> </AppContainer>, document.getElementById('root'), ); }; renderApp(Root); if (module.hot) { module.hot.accept('./containers/Root', () => { renderApp(Root); }); }
// ./containers/Root.js import React from 'react'; import { Provider } from 'react-redux'; import configureStore from '../store/configureStore'; import App from './App'; import '../styles/index.scss'; const store = configureStore(); export default class Root extends React.Component { render() { return ( <Provider store={store}> <App /> </Provider> ); } }
が、Redux の store が絡むとうまくいかないのか、上記のようにすると HMR 時にこのようなエラーが発生した。
does not support changing store
on the fly. It is most likely that you see this error because you updated to Redux 2.x and React Redux 2.x which no longer hot reload reducers automatically. See https://github.com/reactjs/react-redux/releases/tag/v2.0.0 for the migration instructions.
調べてみたところ
Hot reload not working · Issue #502 · reactjs/react-redux
こちらの Issue が見つかり、さらにそこから redux-devtools の TodoMVC の example が 更新されていた のを見つけたので
example のコードを確認し、解決した。
また React Hot Loader の Starter Kit で紹介されている大量のリポジトリの中から
なんかを読むと、(src/client/index.js の部分)
import 'react-hot-loader/patch'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Provider } from 'react-redux'; import { AppContainer } from 'react-hot-loader'; import configureStore from './store/configureStore'; import App from './containers/App'; import './styles/index.scss'; const store = configureStore(); const rootEl = document.getElementById('root'); const renderApp = () => { render( <AppContainer> <Provider store={store}> <App /> </Provider> </AppContainer>, rootEl, ); }; renderApp(); if (module.hot) { module.hot.accept('./containers/App', () => { setImmediate(() => { unmountComponentAtNode(rootEl); renderApp(); }); }); }
というように一旦アンマウントしてから再描画でもうまくいくらしい。本当にこれが正しいのかは不明。