If you’re using Create React App (CRA) to scaffold and develop your app you’re familiar with its reloading feature. Whenever you change a component file for example, the whole application gets reloaded automatically just as you would hit the refresh manually. This is a nice feature when you start developing, but as soon as your app gets more complex and has various states and routes for example, it becomes quite cumbersome to put the app in the state it was before the reload. For example, if you need to login to access a dashboard view, you may lose the logged in status and need to put in your credentials every time.
Hot Module Replacement to the rescue 🔥
Luckily, webpack, which features the development server of CRA, can exchange modules while an app is running without reloading the entire app. For CSS files, CRA already supports this kind of exchanging components in a running app. If you change your stylesheet, you notice that the new styles get loaded and applied, but the app still keeps its state, awesome :)
You can also enable this feature for React Components very very easily if you’re using redux. With the latest version of CRA it’s just a few lines of code you need to adjust. But before you start cheering, please note that with this small adjustments the local state of components, so things which are set with setState()
, will still be lost.
Adjusting CRA
The index.js
file needs to be changed in the following way:
...
const store = configureStore();
...
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
// add these lines
if (module.hot && process.env.NODE_ENV !== 'production') {
module.hot.accept();
}
This makes sure, that the changes of a component file which bubble up to the <App/>
component, get captured here and are accepted.
Now the reducers need to be addressed as well. In the last snippet it is shown that we configure the store by using a function, which is in the file configureStore.js
. In this file, configureStore()
can look like this:
...
const configureStore = () => {
const store = createStore(reducer, initialState, applyMiddleware(
// your middlewares
));
if (module.hot) {
module.hot.accept('./reducers', () => {
const nextRootReducer = require('./reducers').default;
store.replaceReducer(nextRootReducer);
});
}
return store;
};
The path ./reducers
points to the root reducer with a default export. So as soon as some reducer changes, this changes bubbles up to the root one, gets captured here and we replace the old root reducer with a newly required one.
Caveat: Injecting tap events
If you use react-tap-event-plugin
, which is suggested for the material-ui library, you usually put this call in your index.js
.
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
But when using HMR, the index.js
can get loaded more than once, which would inject this plugin more than once as well.
A workaround is to put this call in a separate module injectTapEventSetup.js
and require it in the index.js
file, so when the last one gets loaded again, the unchanged injectTapEventSetup.js
stays untouched.
//injectTapEventSetup.js
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
//index.js
// your imports go here
require('./injectTapEventSetup');
...
Wrapping it up
When you are using Redux with CRA, it kinda shocked me how easy it is to setup HMR today. It’s just so little code you need to add while the benefits are great, especially if you need to do various non dramatic changes to your app. Surely, the local state of a component still gets lost, but since this state is often handled in a non-transparent and inconsistent way compared to Redux, it is much more complex to integrate.