react-hot-loader: Asynchronous React Router v3 routes fail to hot-reload

Hey there! I’m having some difficulty getting react-hot-loader@3.0.0-beta.1 to hot-reload changes to views that are passed to React Router as lazy-loaded POJOs. Updating a view class triggers the usual console messages and throws the warning about changing <Router history>, but the rendered output doesn’t refresh. Curiously, though, changing the components that the views require immediately hot-reloads them, even within the selfsame non-reloading async views.

Here’s a quick screencast demonstrating the issue:

…and here’s the code base that screencast’s taken from: https://github.com/phyllisstein/hildy/tree/react-hot-3. (The master branch of which repo is still using react-transform-hmr without any evident issues.)

I’d be grateful for any suggestions you can provide, and happy to offer more troubleshooting info if I can! Thanks for looking into this.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 12
  • Comments: 32 (4 by maintainers)

Commits related to this issue

Most upvoted comments

I ran into the problem with hot reloading async routes and updated by additionally registering amodule.hot.accept in the require.ensure() callbacks. E.g. like this

getChildRoutes(partialNextState, callback) {
  require.ensure([], function (require) {

    if (module.hot) {
      module.hot.accept('../routes/Blog', () => 
        callback(null, [require('../routes/Blog').default]);
    }

    callback(null, [
      require('../routes/Blog').default,
    ]);
  });
},

Is there any update regarding this issue?

So far there has been only a few suggestions, each having its own drawbacks and all being but workarounds:

Provide “key” prop to <Route />

Fixed the problem, but as mentioned causes deep re-render, therefore loosing components’ state. However, you can find this workaround in officially listed examples of react-hot-loader (i.e. here).

Using asyncComponent from react-async-component

As suggested here, it is possible to wrap the modules you wish to load async with asyncComponent and pass them as component prop to Route directly. This will eliminate hot-reloading issue, however forces you to create additional files doing nothing but resolving modules async.

Various other workarounds

Which are not meant for good code maintenance.

So, it this somehow being solved, or already has been solved with the new release of react-hot-loader?

@flut1 yeah that will allow updates to happen, but component state is lost because it’s deeply re-rendering all of the components, so it’s not the ideal workaround.

Just wanted to add that I was playing around with require.ensure and RR, using @sthzg’s solution, and it seems to work fine with getChildRoutes, but not getComponent.

It looks like the getComponent callback can’t be called multiple times (tested out with setTimeout loading a different component). Another reason to move on to RR4? 😛

@kettanaito You’re right, but read this:

Known limitations

React Router v3 is not fully supported (e.g. async routes). If you want to get most of React Hot Loader, consider switching to React Router v4. If you want to understand the reasoning, it’s good to start in React Router v4 FAQ

And maybe it’s worth a reading: https://github.com/ReactTraining/react-router/tree/v4.0.0-beta.8#why-did-you-get-rid-of-feature-x

I ran into the same issue where HMR was not working with react-router, and I managed to work around the issue, so I thought I’d share my solution:

class SplitComponent extends Component {

  constructor(props) {
    super(props);
    this.state = { Component: false }
  }

  componentWillMount() {
    const { componentPath } = this.props;

    const that = this;

    // chunks will be given file names like:
    // 0.route-PagesRoot.js
    // 2.route-PagesHome.js
    require('bundle-loader?lazy&name=route-[name]!./' + componentPath)(
      Component => {
        that.setState( { Component });
      }
    );
  }

  render() {
    const { Component } = this.state;
    if (Component) {
      return <Component {...this.props}/>;
    }
    else {
      return false;
    }
  }
}

SplitComponent.propTypes = {
  componentPath: PropTypes.string.isRequired
};

function loadComponent(componentPath) {
  return function (location, callback) {
    callback(null, props => <SplitComponent {...props} componentPath={componentPath}/> );
  };
}

Used with the Router like this:

<Router history={browserHistory}>
  <Route path="/" component={App}>
    <IndexRoute component={Home}/>
    <Route path="/pages" getComponent={loadComponent('pages/PagesRoot')}>
      <IndexRoute getComponent={loadComponent('pages/PagesHome')}/>
      <Route path="/pages/:id" getComponent={loadComponent('pages/PagesDetail')}/>
    </Route>
    <Route path="/chapters" getComponent={loadComponent('chapters/ChaptersRoot')}>
      <IndexRoute getComponent={loadComponent('chapters/ChaptersHome')}/>
      <Route path="/chapters/:id" getComponent={loadComponent('chapters/ChaptersDetail')}/>
    </Route>
  </Route>
</Router>

I worked around the getComponent() issue by adding a key prop to Router that changes on each hot update. This will force a different Router instance to be created, and re-executing getComponent() calls. This also fixed the You cannot change <Router router> warnings. Not sure if creating new Router instances causes other (performance) issues but it seems to work for me.

// create key variable
let routerKey = 0;

// initial router rendering
<Router key={routerKey} ... />

if (module.hot) {
 module.hot.accept('./containers/Root', () => {
  ...
  // first bump the key
  renderKey++;
  // re-render
  render(
    <AppContainer>
       ...
      <Router key={renderKey} ... />
    </AppContainer>
  , mountNode);
 });
}

With webpack 2 in System.import HMR too doesn’t work.

I close it. React Router v4 is supported and stable.

@morajabi sorry, there is nothing about SSR in the migration guide. Regardless, the issue with v3 still remains, and there should be some statement how to cope with it.

@morajabi I haven’t got enough time to read about it. So far, I don’t see how to migrate my code to use react-router 4, especially since my SSR relies on match, which was removed from the fourth version.

Using anything that is not an ES6 import is probably the biggest complicating factor for hot reloading.

Also note that, if you save the require('./About').default in the component property, it will never ever get updated if you don’t write code to do it. This would also be the case with ES6 import though (imports are live, but if you save a copy of the current value elsewhere the copy isn’t).

Grain of salt and all, but it seems this issue has mysteriously resolved itself with Webpack 2.2.0. Previously, in order to make hot reloading work, I was manually adding chunks of code like this to any parent view that asynchronously loaded children:

if (process.env.NODE_ENV === 'development') {
  require('./child1');
  require('./child2');
  require('./child3');
}

I removed all such lines on a whim to see what it’d break, and much to my surprise and delight, HMR continued to function beautifully.

I don’t think anything relevant has changed in my setup other than the Webpack version, but this wasn’t carefully tested. Would be glad to see if others could corroborate my story.

@dferber90 the main point of having a chunking in development is the profiling, f.e. with help of such tools like webpack-dashboard. That allows for instance immediately see an impact of the changes to files size without necessity to build and switch to prod. So the drawback is pretty much serious.

Anyway what’s a main bottleneck in this issue making only pure stateless components reloaded in chunks? Router, react-hot-reloader or bundler?

Regarding ‘…’ character in my snippet - that’s just skipped pathes to component files, nothing else