New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CRA v1 + Hot Module Reload (HMR) + Redux #2317

Closed
ro-savage opened this Issue May 22, 2017 · 17 comments

Comments

Projects
None yet
@ro-savage
Contributor

ro-savage commented May 22, 2017

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
@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon May 22, 2017

Member

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
Member

gaearon commented May 22, 2017

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
@bfncs

This comment has been minimized.

Show comment
Hide comment
@bfncs

bfncs Jun 11, 2017

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.

bfncs commented Jun 11, 2017

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.

@gaearon gaearon closed this Jun 26, 2017

@dreyks

This comment has been minimized.

Show comment
Hide comment
@dreyks

dreyks Jul 25, 2017

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

dreyks commented Jul 25, 2017

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

@rmoorman

This comment has been minimized.

Show comment
Hide comment
@rmoorman

rmoorman Jul 27, 2017

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 commented Jul 27, 2017

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 ...

@nealoke

This comment has been minimized.

Show comment
Hide comment
@nealoke

nealoke Jul 27, 2017

@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...

nealoke commented Jul 27, 2017

@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...

@nealoke

This comment has been minimized.

Show comment
Hide comment
@nealoke

nealoke Jul 27, 2017

@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 commented Jul 27, 2017

@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;
@rmoorman

This comment has been minimized.

Show comment
Hide comment
@rmoorman

rmoorman Jul 27, 2017

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

rmoorman commented Jul 27, 2017

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

@zanjs

This comment has been minimized.

Show comment
Hide comment
@zanjs

zanjs Aug 5, 2017

Thinks 😜

zanjs commented Aug 5, 2017

Thinks 😜

@sdhhqb

This comment has been minimized.

Show comment
Hide comment
@sdhhqb

sdhhqb Aug 8, 2017

thanks! these help a lot!

sdhhqb commented Aug 8, 2017

thanks! these help a lot!

@nealoke

This comment has been minimized.

Show comment
Hide comment
@nealoke

nealoke Aug 8, 2017

@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 commented Aug 8, 2017

@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?

@sdhhqb

This comment has been minimized.

Show comment
Hide comment
@sdhhqb

sdhhqb Aug 8, 2017

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

sdhhqb commented Aug 8, 2017

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

@rmoorman

This comment has been minimized.

Show comment
Hide comment
@rmoorman

rmoorman Aug 8, 2017

@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.

rmoorman commented Aug 8, 2017

@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.

@gnapse

This comment has been minimized.

Show comment
Hide comment
@gnapse

gnapse Sep 27, 2017

Contributor

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?

Contributor

gnapse commented Sep 27, 2017

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?

@onpaws

This comment has been minimized.

Show comment
Hide comment
@onpaws

onpaws Oct 6, 2017

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

onpaws commented Oct 6, 2017

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

@RavenHursT

This comment has been minimized.

Show comment
Hide comment
@RavenHursT

RavenHursT Aug 9, 2018

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 commented Aug 9, 2018

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 :-)

@jamesg1

This comment has been minimized.

Show comment
Hide comment
@jamesg1

jamesg1 Aug 23, 2018

@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.

jamesg1 commented Aug 23, 2018

@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.

@RavenHursT

This comment has been minimized.

Show comment
Hide comment
@RavenHursT

RavenHursT Aug 23, 2018

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

RavenHursT commented Aug 23, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment