概要
React + ReduxでTypescriptを使ってHot Module Replacement (HMR)する際のメモ。
なんでこれでOKなのかは詳しく調べないと分からず。
最小構成のプロジェクトを別途作成する。
Webpack-dev-serverを使う前提
準備
npm install --save-dev babel-loader @babel/core react-hot-loader @types/webpack-env
モジュール | 導入理由 | 補足 |
---|---|---|
babel-loader | react-hot-loaderが使う | *1 |
@babel/core | babel-loaderが利用 | |
react-hot-loader | Reactのstateを維持できるようにする | |
@types/webpack-env | hot.moduleの型を使えるように | |
*1 TypeScriptだとbabel必要なかったのだが仕方なし。 |
Webpackの設定
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') .BundleAnalyzerPlugin;
module.exports = (env, argv) => {
let config = {
performance: { hints: false },
entry : { app:[
// 必要性はよくわからず。
'webpack-dev-server/client?http://localhost:8080',
// 必要性はよくわからず
'webpack/hot/only-dev-server',
'./dist/index.js'
]},
output: {
path : path.join(__dirname,'public/js'),
filename: '[name].js',
publicPath: '/js/'
},
plugins: [
// NamedModulesPluginはなくていいみたい。
//new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
],
devServer: {
historyApiFallback: true,
contentBase : path.join(__dirname,'public'),
port : 8080,
hot : true,
publicPath : '/js/',
watchOptions : {
aggregateTimeout: 300,
poll : 1000
}
},
resolve: {
modules: [path.resolve(__dirname, './dist'), 'node_modules'],
alias : {
//省略
},
extensions: [".ts", ".tsx", ".js", ".json"]
},
module: {
rules: [
{
exclude: /node_modules/,
test : /\.js$/,
//loader : ['react-hot-loader/webpack', 'babel-loader']
//hot-loader/webpackはいらない様子。
// 入れるとmonorepo全部にhot-loaderのinstallを求められた。
loader : ['babel-loader']
}
]
}
};
if (argv.mode === 'production') {
//本番環境専用の設定
//ファイルサイズのアナライズを実行
//config.plugins.push(new BundleAnalyzerPlugin());
} else {
//開発環境専用の設定
config.cache = true;
config.devtool = 'inline-source-map';
}
return config;
};
Componentの設定
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.querySelector('#root'));
App.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { configureStore } from './ConfigureStore';
import Navigator from './Navigator';
const store = configureStore();
const App = () => {
return (
<Provider store={store}>
<Navigator />
</Provider>
);
};
export default App; //Appをhot()で囲ったら動かなかった。Providerの内側にする必要があるみたい。
Navigator.tsx
import React from 'react';
import { hot } from 'react-hot-loader/root';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import AuthPage from './AuthPage';
import TopPage from './TopPage';
const Navigator = () => {
return (
<BrowserRouter>
<Switch>
<Route exact={true} path='/login' component={AuthPage} />
<Route exact={true} path='/top' component={TopPage} />
</Switch>
</BrowserRouter>
);
};
export default hot(Navigator); // なのでNavigatorをhotで括った
Reducerの設定
StoreConfigure.ts
import { createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
// 後述する通り、RootReducerはexport defaultした方が良さそう
import rootReducer from './RootReducer';
// Sagaもif module.hotしないと変更に追従できなそう。
import rootSaga from './RootSaga';
export const configureStore = (initialState = {}) => {
if (module.hot) {
module.hot.accept('./RootReducer', () => {
console.log('module.hot.accept');
// RootReducerはexport defaultしてるので、require.default
// RootReducerをexportにして、requireに変えたら動かなかった様子。
const nextRootReducer = require('./RootReducer').default;
store.replaceReducer(nextRootReducer);
});
}
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, initialState, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
return store;
};
GitHub
未作成