Create-react-app: CRA v1 + Hot Module Reload (HMR) + Redux

Created on 22 May 2017  路  17Comments  路  Source: facebook/create-react-app

I'd like to confirm the 'correct' way to have CRA + HMR + React now we are on version 1 and using webpack 2, and that all these thoughts are correct. They may be useful to others adding HMR.

Examples have been updated incorporating feedback

Why Hot Module Reload

Adding in HMR changes your application from full page refresh to refreshing just the app.
In most cases this will make the page reload faster.

If you are using external state management such as Redux, you can have your redux state remain when component changes are made.

Note: This is not same a component based hot-module-reloading where state within your react application remains unchanged when making changes to your source code. HMR will remove any component-based state. That is currently unsupported by CRA, more information see react-hot-loader and status post by gaereon.

Hot Module Reload without Redux

index.js

// regular imports
ReactDOM.render(<App /> , document.getElementById('root'))

if (module.hot) {
  module.hot.accept('./App', () => {
    ReactDOM.render(<App />, document.getElementById('root'))
  })
}

As seen here and in issue #897

Hot Module Reload with Redux (or similar state management)

index.js

// Normal imports
import { Provider } from 'react-redux'
import configureStore from './redux/configureStore'

const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
  , document.getElementById('root'))

if (module.hot) {
  module.hot.accept('./App', () => {
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root'),
    )
  })
}

configureStore.js (or similar)

import { createStore } from 'redux'

import rootReducer from './reducers'

const configureStore = () => {
  const store = createStore(rootReducer)

  if (process.env.NODE_ENV !== "production") {
    if (module.hot) {
      module.hot.accept('./reducers', () => {
        store.replaceReducer(rootReducer)
      })
    }
  }

  return store
}

export default configureStore
question

Most helpful comment

With Webpack 2 (which CRA now uses) this should be enough:

// regular imports
ReactDOM.render(<App /> , document.getElementById('root'))

if (module.hot) {
  module.hot.accept('./App', () => {
    ReactDOM.render(<App />, document.getElementById('root'))
  })
}

It's because App is live-bound to ES6 import in Webpack 2.

Same with the other example:

import { createStore } from 'redux'

import rootReducer from './reducers'

const configureStore = () => {
  const store = createStore(rootReducer)

  if (process.env.NODE_ENV !== "production") {
    if (module.hot) {
      module.hot.accept('./reducers', () => {
        store.replaceReducer(rootReducer)
      })
    }
  }

  return store
}

export default configureStore

All 17 comments

With Webpack 2 (which CRA now uses) this should be enough:

// regular imports
ReactDOM.render(<App /> , document.getElementById('root'))

if (module.hot) {
  module.hot.accept('./App', () => {
    ReactDOM.render(<App />, document.getElementById('root'))
  })
}

It's because App is live-bound to ES6 import in Webpack 2.

Same with the other example:

import { createStore } from 'redux'

import rootReducer from './reducers'

const configureStore = () => {
  const store = createStore(rootReducer)

  if (process.env.NODE_ENV !== "production") {
    if (module.hot) {
      module.hot.accept('./reducers', () => {
        store.replaceReducer(rootReducer)
      })
    }
  }

  return store
}

export default configureStore

Really helpful, thanks a lot! Note, that the component you are hot reloading (<App /> in your example) needs to be class component, it will not work with a pure functional component.

for some weird reason my state gets wiped clean on hot reloading. I'm using plain react, no redux

Same here (but using a simple redux setup). When I change my App component, the state seems to be gone.
@ro-savage, @bfncs or @gaearon is there some working example on github that can be cloned and run (CRA + minimal redux)?

Edit: suddenly it started working ... and I have no idea why ...

@rmoorman would you be able to share a small repo where all the code is present? I've been trying to get this to work for a couple of hours now and can't succeed...

I can see that the HMR picks up the change in the rootReducer and it recompiles fine. But when I check to see if the changed reducer actually changed, I can see that it does not change anything.

Any help would be awesome...

@bfncs @gaearon Am I missing something here?

index.js

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { AppContainer } from 'react-hot-loader';
import { Router, browserHistory } from 'react-router';

import configureStore from 'state/store';
import routes from 'routing/routes.js';

export const store = configureStore();

const App = () => (
    <Provider store={store}>
        <Router history={browserHistory} routes={routes} />
    </Provider>
);

const renderApp = Component => {
    render(
        <AppContainer>
            <Component />
        </AppContainer>,
        document.getElementById('root')
    )
};

renderApp(App);

if (module.hot) {
    module.hot.accept(App, () => renderApp(App));
}

Store.js

import { createStore, applyMiddleware, compose } from 'redux';

import { reduxBatch } from '@manaflair/redux-batch';
import ReduxThunk from 'redux-thunk';

import orm from './orm';
import bootstrapper from './bootstrapper';
import reducers from './reducers';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

export const configureStore = () => {
    const store = createStore(reducers, bootstrapper(orm), composeEnhancers(reduxBatch, applyMiddleware(ReduxThunk), reduxBatch));

    if (process.env.NODE_ENV !== 'production') {
        if (module.hot) {
            module.hot.accept('./reducers', () => {
                const test = require('./reducers');
                store.replaceReducer(test);
            });
        }
    }

    return store;
};

export default configureStore;

@nealoke here you go. Pretty basic. Whenever you change the app component (or the rootReducer), state is kept.

Thinks 馃槣

thanks! these help a lot!

@rmoorman thanks, but I can't find a difference in setup between yours and my snippet though 馃槥

@sdhhqb did you get it to work with reducers as well?

@nealoke yes, it's working, the store can update when I change reducers. I use gaearon's approach.

@nealoke but does cloning the example repo and running the code work for you (the example is basically what @gearon suggested)? I would suggest to take one of the working approaches and incrementally go from there towards your piece of code in order to find the culprit.

the component you are hot reloading ( in your example) needs to be class component, it will not work with a pure functional component

@bfncs I just got it to work with a pure functional component. Perhaps something has changed since you made that comment?

@gnapse @bfncs +1 - was also able to use a pure functional <App />.

I'm not sure what I'm doing wrong here...

AFAICT, my index.js is the same as what I'm seeing here (apart from ConnectedRouter).. The app compiles and loads up in the browser w/o any errors.

But when I make changes to App, while I can see the HMR requests complete in the Network tab:

image

... Not only do I not see the changes applied.. But now the page won't even reload. I have to manually reload now, to see said changes.

Here's the content of my index.js, and store.js:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.scss'
import App from './App'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import createStore, { history } from './store'
import registerServiceWorker from './registerServiceWorker'
import 'bootstrap/dist/css/bootstrap.min.css'

const store = createStore()

registerServiceWorker()
const rootElem = document.getElementById('root')
const AppJsx = <Provider store={store}>
  <ConnectedRouter history={history}>
    <div>
      <App />
    </div>
  </ConnectedRouter>
</Provider>

ReactDOM.render(
  AppJsx,
  rootElem
)

if (module.hot) {
  module.hot.accept('./App', () => {
    ReactDOM.render(AppJsx, rootElem)
  })
}
import { createStore, applyMiddleware, compose } from 'redux'
import { connectRouter, routerMiddleware } from 'connected-react-router'
import thunk from 'redux-thunk'
import createHistory from 'history/createBrowserHistory'
import rootReducer from './modules'

export const history = createHistory()
const initialState = {}
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

export default () => createStore(
  connectRouter(history)(rootReducer),
  initialState,
  composeEnhancer(
    applyMiddleware(
      thunk,
      routerMiddleware(history),
    ),
  ),
)

Any help would be greatly appreciated :-)

@RavenHursT I think this blog post will help you here, I found it a big help today for getting HMR to work on my project. https://medium.com/@brianhan/hot-reloading-cra-without-eject-b54af352c642

Your missing hot module replacement for your redux store. Eg:

if (process.env.NODE_ENV !== 'production') {
    if (module.hot) {
      module.hot.accept('./nav/reducers', () => {
        store.replaceReducer(reducer);
      });
    }
  }

I would suggest to also split up your createStore() function parameters.

Cool! I'm gonna try that out! Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

oltsa picture oltsa  路  3Comments

fson picture fson  路  3Comments

xgqfrms-GitHub picture xgqfrms-GitHub  路  3Comments

alleroux picture alleroux  路  3Comments

alleroux picture alleroux  路  3Comments