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

Question: differences between Webpack HMR & React-Hot-Loader ? #1063

Closed
Sharlaan opened this Issue Nov 20, 2016 · 11 comments

Comments

Projects
None yet
3 participants
@Sharlaan

Sharlaan commented Nov 20, 2016

I'm looking forward integration of webpack2 "and" React-Hot-Loader 3 (RHL) in CRA...
... but then i realized i'm not sure to fully understand the differences between webpack's HMR and RHL:

Are they doing same thing ? Are they mutually exclusive, or on contrary they must be used together to prevent full page refresh ?

At the moment, i'm using current CRA which seems to only use HMR: during dev, a change of a simple text or inner component, auto-refreshes the whole page.
Fine but when i saw this RHL video, i was like WoW it's awesome... but then why my CRA (which includes built-in "HMR") doesnot behave like that o.O ?!

I'm kinda confused, could someone explain the differences to get similar dev experience with CRA like in RHL video please ?

P.S.: not sure if that is pertinent, i'm using Webstorm 2016.3

@Sharlaan Sharlaan changed the title from Question: differences between Webpack HMR & React-Hot-Reload ? to Question: differences between Webpack HMR & React-Hot-Loader ? Nov 20, 2016

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Nov 20, 2016

Member

If you want to fully understand the topic in depth I wrote something about it.

Webpack HMR is the underlying mechanism for replacing JS modules on the fly. It offers an API for doing so (module.hot.accept) but it requires that you explicitly "mark" the modules that can be updated on the fly, and specify how those updates should be handled.

HMR API is available in Create React App. It means you can write code like this:

a.js

const b = require('./b');
alert('At first b is ' + b);

if (module.hot) {
  module.hot.accept('./b', function() {
    const updatedB = require('./b');
    alert('But now it is ' + updatedB);
  });
}

b.js

module.exports = 42; // edit to make it 43

This is all HMR API gives you: an ability for the app to handle "new versions" of the same module and do something with it.

React Hot Loader is a project that attempts to build on top of Webpack HMR and preserve DOM and React component state when components are saved. Normally it's hard to achieve with vanilla Webpack HMR because if you just replace a module, React will think it's a different component type and destroy DOM and local state. So React Hot Loader does many tricks attempting to prevent this, but it's also way more complex than underlying HMR API.

At the moment, i'm using current CRA which seems to only use HMR: during dev, a change of a simple text or inner component, auto-refreshes the whole page.

This is not "HMR". This is just a refresh. "HMR" is what you see in CSS: when you change CSS file, it updates without refreshing. This is because style-loader (which is what we use for styles in development) calls HMR API to replace stylesheets on change.

The reason you don't get HMR behavior even though it's enabled in Create React App is because you haven't written any module.hot.accept calls. If Webpack doesn't find them traversing the "import chain" up, it decides to refresh the page. But if all changed modules are handled with module.hot.accept somewhere, it doesn't refresh.

Of course manually adding module.hot.accept calls in all modules for React components would be incredibly tedious. This is why React Hot Loader was created, so that it can generate that code for you. But it's hard to do right, and React Hot Loader 1.x had many issues, and is now unsupported. 3.x is a ground-up rewrite that embraces HMR API and actually asks you to write a single module.hot.accept call at the very top of the app. Unfortunately it's also not stable yet. You can contribute to it. :-)

I'm kinda confused, could someone explain the differences to get similar dev experience with CRA like in RHL video please ?

You can get reasonably close with this approach. However, unlike React Hot Loader, it wouldn't preserve the component local state or DOM, so it's just a "faster reload".

There is no way to fully have RHL-like experience in CRA. I hope to gradually get there but it's a longer road. See gaearon/react-hot-boilerplate#97 (comment) for details.

I hope this helps!

Member

gaearon commented Nov 20, 2016

If you want to fully understand the topic in depth I wrote something about it.

Webpack HMR is the underlying mechanism for replacing JS modules on the fly. It offers an API for doing so (module.hot.accept) but it requires that you explicitly "mark" the modules that can be updated on the fly, and specify how those updates should be handled.

HMR API is available in Create React App. It means you can write code like this:

a.js

const b = require('./b');
alert('At first b is ' + b);

if (module.hot) {
  module.hot.accept('./b', function() {
    const updatedB = require('./b');
    alert('But now it is ' + updatedB);
  });
}

b.js

module.exports = 42; // edit to make it 43

This is all HMR API gives you: an ability for the app to handle "new versions" of the same module and do something with it.

React Hot Loader is a project that attempts to build on top of Webpack HMR and preserve DOM and React component state when components are saved. Normally it's hard to achieve with vanilla Webpack HMR because if you just replace a module, React will think it's a different component type and destroy DOM and local state. So React Hot Loader does many tricks attempting to prevent this, but it's also way more complex than underlying HMR API.

At the moment, i'm using current CRA which seems to only use HMR: during dev, a change of a simple text or inner component, auto-refreshes the whole page.

This is not "HMR". This is just a refresh. "HMR" is what you see in CSS: when you change CSS file, it updates without refreshing. This is because style-loader (which is what we use for styles in development) calls HMR API to replace stylesheets on change.

The reason you don't get HMR behavior even though it's enabled in Create React App is because you haven't written any module.hot.accept calls. If Webpack doesn't find them traversing the "import chain" up, it decides to refresh the page. But if all changed modules are handled with module.hot.accept somewhere, it doesn't refresh.

Of course manually adding module.hot.accept calls in all modules for React components would be incredibly tedious. This is why React Hot Loader was created, so that it can generate that code for you. But it's hard to do right, and React Hot Loader 1.x had many issues, and is now unsupported. 3.x is a ground-up rewrite that embraces HMR API and actually asks you to write a single module.hot.accept call at the very top of the app. Unfortunately it's also not stable yet. You can contribute to it. :-)

I'm kinda confused, could someone explain the differences to get similar dev experience with CRA like in RHL video please ?

You can get reasonably close with this approach. However, unlike React Hot Loader, it wouldn't preserve the component local state or DOM, so it's just a "faster reload".

There is no way to fully have RHL-like experience in CRA. I hope to gradually get there but it's a longer road. See gaearon/react-hot-boilerplate#97 (comment) for details.

I hope this helps!

@gaearon gaearon closed this Nov 20, 2016

@Sharlaan

This comment has been minimized.

Show comment
Hide comment
@Sharlaan

Sharlaan Nov 20, 2016

Thanks for the deep explanations, they were a long read but were what i was searching for, thanks !

FOr now, i'll just follow your advice to activate Webpack HMR only till you or someone else gets this to work, quoting the 4th point of your plan:

Add the necessary hooks to React to avoid hacks like this, a top-level , or module.hot.accept code. The user shouldn’t change their code in any way for hot reloading to work. Incidentally we need the same hooks for React DevTools so it aligns with React team plans.

Speaking about folowing the adviced approach linked in your answer, it works for a barebone React Project but... real projects uses a router, an internationalization, and CSS framework, most of the time i think.

So for instance, my project's index.js looks like that:

import React from 'react'
import { render } from 'react-dom'
import Provider from 'react-redux/lib/components/Provider'
import Router from 'react-router/lib/Router'
import browserHistory from 'react-router/lib/browserHistory'
import './index.css'

import routes from './routes'
import store from './store'

const rootEl = document.getElementById('root')

render(
  <Provider store={store}>
    <Router history={browserHistory} routes={routes} />
  </Provider>,
  rootEl
)

// activates Webpack's HotModuleReload
if (module.hot) {
  module.hot.accept('./components/App', () => {
    const NextApp = require('./components/App').default
    render(
      <NextApp />,
      rootEl
    )
  })
}

... it doesnot change anything, still full page reload. Btw it takes about 2secs to recompile, then 2 other for the browser to refresh. That's why i was really astonished when seeing your video with instant change refresh.
I also tried the module.hot condition in routes.js but still doesnot improve anything.

What am i doing wrong ?

PS: my App component is actually the base one in routes.js

Sharlaan commented Nov 20, 2016

Thanks for the deep explanations, they were a long read but were what i was searching for, thanks !

FOr now, i'll just follow your advice to activate Webpack HMR only till you or someone else gets this to work, quoting the 4th point of your plan:

Add the necessary hooks to React to avoid hacks like this, a top-level , or module.hot.accept code. The user shouldn’t change their code in any way for hot reloading to work. Incidentally we need the same hooks for React DevTools so it aligns with React team plans.

Speaking about folowing the adviced approach linked in your answer, it works for a barebone React Project but... real projects uses a router, an internationalization, and CSS framework, most of the time i think.

So for instance, my project's index.js looks like that:

import React from 'react'
import { render } from 'react-dom'
import Provider from 'react-redux/lib/components/Provider'
import Router from 'react-router/lib/Router'
import browserHistory from 'react-router/lib/browserHistory'
import './index.css'

import routes from './routes'
import store from './store'

const rootEl = document.getElementById('root')

render(
  <Provider store={store}>
    <Router history={browserHistory} routes={routes} />
  </Provider>,
  rootEl
)

// activates Webpack's HotModuleReload
if (module.hot) {
  module.hot.accept('./components/App', () => {
    const NextApp = require('./components/App').default
    render(
      <NextApp />,
      rootEl
    )
  })
}

... it doesnot change anything, still full page reload. Btw it takes about 2secs to recompile, then 2 other for the browser to refresh. That's why i was really astonished when seeing your video with instant change refresh.
I also tried the module.hot condition in routes.js but still doesnot improve anything.

What am i doing wrong ?

PS: my App component is actually the base one in routes.js

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Nov 20, 2016

Member

What am i doing wrong ?

NextApp is not a "magic" thing, it's your App component. But you don't have an App component in the example.

You likely want something like this?

import React from 'react'
import { render } from 'react-dom'
import Provider from 'react-redux/lib/components/Provider'
import Router from 'react-router/lib/Router'
import browserHistory from 'react-router/lib/browserHistory'

// Note: I can't see where you defined them so I added this:
import routes from './routes';

const rootEl = document.getElementById('root')

render(
  <Provider store={store}>
    <Router history={browserHistory} routes={routes} />
  </Provider>,
  rootEl
)

if (module.hot) {
  // This is not a magic thing!
  // You need to put whatever changes in *your* project here.
  module.hot.accept('./routes', () => {
    const nextRoutes = require('./routes').default // Again, depends on your project
    render(
      <Provider store={store}>
        <Router history={browserHistory} routes={nextRoutes} />
      </Provider>,
      rootEl
    )
  })
}

As I explained above module.hot.accept is not a magic block, you need to tell it what to do.

Member

gaearon commented Nov 20, 2016

What am i doing wrong ?

NextApp is not a "magic" thing, it's your App component. But you don't have an App component in the example.

You likely want something like this?

import React from 'react'
import { render } from 'react-dom'
import Provider from 'react-redux/lib/components/Provider'
import Router from 'react-router/lib/Router'
import browserHistory from 'react-router/lib/browserHistory'

// Note: I can't see where you defined them so I added this:
import routes from './routes';

const rootEl = document.getElementById('root')

render(
  <Provider store={store}>
    <Router history={browserHistory} routes={routes} />
  </Provider>,
  rootEl
)

if (module.hot) {
  // This is not a magic thing!
  // You need to put whatever changes in *your* project here.
  module.hot.accept('./routes', () => {
    const nextRoutes = require('./routes').default // Again, depends on your project
    render(
      <Provider store={store}>
        <Router history={browserHistory} routes={nextRoutes} />
      </Provider>,
      rootEl
    )
  })
}

As I explained above module.hot.accept is not a magic block, you need to tell it what to do.

@Sharlaan

This comment has been minimized.

Show comment
Hide comment
@Sharlaan

Sharlaan Nov 21, 2016

Still 4secs full page reload :s

I'm wondering if i forgot to install something else besides CRA itself ?

Here 's an example of my routes.js

import React from 'react'
import { Route, IndexRoute } from 'react-router'
import App from './components/App'
import HelloPage from './components/HelloPage'

export default (
  <Route path='/' component={App}>
    <IndexRoute component={HelloPage} />
  </Route>
)

...and the HelloPage is a dumb Component

import React from 'react'
export default () => <p>Hello world! This is the home page route.</p>

If i modify "Hello" with "Yop" it doesnot "autoreflect" in browser, but if i save it triggers a full page reload.
Is it expected behavior ?

I've read in your long article ReactTransform and RHL had troubles detecting components not using class extends Component or React.createClass(), so i tried also with HelloPage with class: still same behavior :s

I also tried only with just "npm start" from Windows CLI and modify in SublimeText (instead of Webstorm, thinking maybe something in the IDE would prevent autorefresh) but still same behavior.

Did i understood correctly when i read adding module.hot condition in topmost component in the tree would be enugh to catch any change bubbling up ?
so "routes" injected in component should bubble and trigger the "silent change" through nextRoutes right ?

PS: forgot to mention CRA ask me if i want to change port on start, since port3000 is already used by my API, not sure if this could have an impact on the problem ?

Sharlaan commented Nov 21, 2016

Still 4secs full page reload :s

I'm wondering if i forgot to install something else besides CRA itself ?

Here 's an example of my routes.js

import React from 'react'
import { Route, IndexRoute } from 'react-router'
import App from './components/App'
import HelloPage from './components/HelloPage'

export default (
  <Route path='/' component={App}>
    <IndexRoute component={HelloPage} />
  </Route>
)

...and the HelloPage is a dumb Component

import React from 'react'
export default () => <p>Hello world! This is the home page route.</p>

If i modify "Hello" with "Yop" it doesnot "autoreflect" in browser, but if i save it triggers a full page reload.
Is it expected behavior ?

I've read in your long article ReactTransform and RHL had troubles detecting components not using class extends Component or React.createClass(), so i tried also with HelloPage with class: still same behavior :s

I also tried only with just "npm start" from Windows CLI and modify in SublimeText (instead of Webstorm, thinking maybe something in the IDE would prevent autorefresh) but still same behavior.

Did i understood correctly when i read adding module.hot condition in topmost component in the tree would be enugh to catch any change bubbling up ?
so "routes" injected in component should bubble and trigger the "silent change" through nextRoutes right ?

PS: forgot to mention CRA ask me if i want to change port on start, since port3000 is already used by my API, not sure if this could have an impact on the problem ?

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Nov 21, 2016

Member

It's hard to say anything without an example reproducing your problem.

Member

gaearon commented Nov 21, 2016

It's hard to say anything without an example reproducing your problem.

@Sharlaan

This comment has been minimized.

Show comment
Hide comment
@Sharlaan

Sharlaan Nov 21, 2016

Ok :

  1. started a new empty webstorm 2016.3 project
  2. cli => creat-react-app crahmr (wait ~20mins till all is installed)
  3. cd crahmr => npm i -S react-router
    should get these versions :
"devDependencies": {
    "react-scripts": "0.7.0"
  },
  "dependencies": {
    "react": "^15.4.0",
    "react-dom": "^15.4.0",
    "react-router": "^3.0.0"
  }
  1. modify index.js with:
import React from 'react'
import { render } from 'react-dom'
import Router from 'react-router/lib/Router'
import browserHistory from 'react-router/lib/browserHistory'
import routes from './routes'

const rootEl = document.getElementById('root')

render(
  <Router history={browserHistory} routes={routes} />,
  rootEl
)

/*
if (module.hot) {
  module.hot.accept('./App', () => {
    console.debug('HMR !')
    const nextRoutes = require('./routes').default
    render(
      <Router history={browserHistory} routes={nextRoutes} />,
      rootEl
    )
  })
}
*/
  1. modify App.js with :
import React from 'react'
import logo from './logo.svg'
import './App.css'

export default ({children}) => (
 <div className='App'>
   <div className='App-header'>
     <img src={logo} className='App-logo' alt='logo' />
     <h2>Welcome to React</h2>
   </div>
   <p className='App-intro'>
     To get started, edit <code>src/App.js</code> and save to reload.
   </p>
   {children}
 </div>
)
  1. create routes.js :
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import App from './App'
import HelloPage from './HelloPage'

export default (
  <Route path='/' component={App}>
    <IndexRoute component={HelloPage} />
  </Route>
)
  1. create HelloPage.js :
import React from 'react'
export default () => <p style={{color: 'CornFlowerBlue'}}>Hello world !</p>
  1. npm start

expected results :

It displays the expected black Header with spinning React logo
It displays the expected 2 sentences from App and HelloPage
Change a word in HelloPage triggers a full refresh (including the unmodified Header and logo)

HMR attempt :

Now i uncomment in index.js the hmr condition and save (it recompiles)
I modify some word in HelloPage and save => full refresh including unmodified components
Same with changing inline color in HelloPage style => full refresh

It should NOT refresh the unmodified components, ie Header, logo, etc... => fail
Console should display "HMR !" => fail

Sharlaan commented Nov 21, 2016

Ok :

  1. started a new empty webstorm 2016.3 project
  2. cli => creat-react-app crahmr (wait ~20mins till all is installed)
  3. cd crahmr => npm i -S react-router
    should get these versions :
"devDependencies": {
    "react-scripts": "0.7.0"
  },
  "dependencies": {
    "react": "^15.4.0",
    "react-dom": "^15.4.0",
    "react-router": "^3.0.0"
  }
  1. modify index.js with:
import React from 'react'
import { render } from 'react-dom'
import Router from 'react-router/lib/Router'
import browserHistory from 'react-router/lib/browserHistory'
import routes from './routes'

const rootEl = document.getElementById('root')

render(
  <Router history={browserHistory} routes={routes} />,
  rootEl
)

/*
if (module.hot) {
  module.hot.accept('./App', () => {
    console.debug('HMR !')
    const nextRoutes = require('./routes').default
    render(
      <Router history={browserHistory} routes={nextRoutes} />,
      rootEl
    )
  })
}
*/
  1. modify App.js with :
import React from 'react'
import logo from './logo.svg'
import './App.css'

export default ({children}) => (
 <div className='App'>
   <div className='App-header'>
     <img src={logo} className='App-logo' alt='logo' />
     <h2>Welcome to React</h2>
   </div>
   <p className='App-intro'>
     To get started, edit <code>src/App.js</code> and save to reload.
   </p>
   {children}
 </div>
)
  1. create routes.js :
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import App from './App'
import HelloPage from './HelloPage'

export default (
  <Route path='/' component={App}>
    <IndexRoute component={HelloPage} />
  </Route>
)
  1. create HelloPage.js :
import React from 'react'
export default () => <p style={{color: 'CornFlowerBlue'}}>Hello world !</p>
  1. npm start

expected results :

It displays the expected black Header with spinning React logo
It displays the expected 2 sentences from App and HelloPage
Change a word in HelloPage triggers a full refresh (including the unmodified Header and logo)

HMR attempt :

Now i uncomment in index.js the hmr condition and save (it recompiles)
I modify some word in HelloPage and save => full refresh including unmodified components
Same with changing inline color in HelloPage style => full refresh

It should NOT refresh the unmodified components, ie Header, logo, etc... => fail
Console should display "HMR !" => fail

@cpunion

This comment has been minimized.

Show comment
Hide comment
@cpunion

cpunion Dec 4, 2016

@Sharlaan Typo in your code:

if (module.hot) {
  module.hot.accept('./App', () => {   // <====== modify './App' to './routes' and it works fine.
    console.debug('HMR !')
    const nextRoutes = require('./routes').default
    render(
      <Router history={browserHistory} routes={nextRoutes} />,
      rootEl
    )
  })
}

cpunion commented Dec 4, 2016

@Sharlaan Typo in your code:

if (module.hot) {
  module.hot.accept('./App', () => {   // <====== modify './App' to './routes' and it works fine.
    console.debug('HMR !')
    const nextRoutes = require('./routes').default
    render(
      <Router history={browserHistory} routes={nextRoutes} />,
      rootEl
    )
  })
}
@cpunion

This comment has been minimized.

Show comment
Hide comment
@cpunion

cpunion Dec 4, 2016

But react-router can't be updated:

2016-12-04 2 53 22

cpunion commented Dec 4, 2016

But react-router can't be updated:

2016-12-04 2 53 22

@cpunion

This comment has been minimized.

Show comment
Hide comment
@cpunion

cpunion Dec 5, 2016

@Sharlaan I tried react-hot-loader@3.0.0-beta.6, hot reloading works, but seems like Re-Render, every components are remounted.

@gaearon I can't correctly configure a PERFECTLY Hot Module Reload project that it can hot reload and keep component's state. When I update a component, all components reload and remount. Is there an example that it doesn't reset the state?

cpunion commented Dec 5, 2016

@Sharlaan I tried react-hot-loader@3.0.0-beta.6, hot reloading works, but seems like Re-Render, every components are remounted.

@gaearon I can't correctly configure a PERFECTLY Hot Module Reload project that it can hot reload and keep component's state. When I update a component, all components reload and remount. Is there an example that it doesn't reset the state?

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Dec 5, 2016

Member

@cpunion Please direct this feedback to React Hot Loader repository. Yes, there is an example, and it's linked for React Hot Loader README's first paragraph. gaearon/react-hot-loader#240

Member

gaearon commented Dec 5, 2016

@cpunion Please direct this feedback to React Hot Loader repository. Yes, there is an example, and it's linked for React Hot Loader README's first paragraph. gaearon/react-hot-loader#240

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Dec 5, 2016

Member

I’m locking this particular thread because it’s not related to Create React App at the moment, and comments will just get lost unaddressed here. Please file issues with the projects you are using for hot reloading.

Member

gaearon commented Dec 5, 2016

I’m locking this particular thread because it’s not related to Create React App at the moment, and comments will just get lost unaddressed here. Please file issues with the projects you are using for hot reloading.

@facebook facebook locked and limited conversation to collaborators Dec 5, 2016

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