React-hot-loader: Only top-level component hot reloads

Created on 3 May 2016  路  24Comments  路  Source: gaearon/react-hot-loader

ETA: This issue actually does involve React Router, after all. Please see my comment further down the thread.

_Previously:_

I'm using 3.0.0-beta.1.

I originally thought the problem was with React Router but I'm updating the description to reflect what my problem actually is. React Router is fine.

Hot module reloading works for my root component and for any lower level components defined within the same module. However, hot module loading doesn't seem to propagate down the import tree. Lower level components defined in separate files end up triggering the warning message, The following modules couldn't be hot updated: (They would need a full reload!).

Not sure what I'm doing wrong. Here's how I'm telling webpack to re-render.

if (module.hot) {
  module.hot.accept('./root', () => {
    const NextRoot = require('./root').default;
    render(
      <AppContainer>
        <NextRoot {...{ store, history, onUpdate, routes }} />
      </AppContainer>,
      document.getElementById('app')
    );
  });
}

Most helpful comment

Sorry, copy-pasting and setting up a project is really tedious to validate bug reports 馃槃
Can you please provide this on GitHub?

All 24 comments

Please provide a complete example reproducing this. Since it doesn鈥檛 happen in https://github.com/gaearon/react-hot-boilerplate/pull/61, it鈥檚 hard to say what鈥檚 going on.

I get this warning (The following modules couldn't be hot updated: (They would need a full reload!)) too.
It seems be related to react-router and react-hot-loader but I don't know why. My full project on implement_react_hot_loader

package.json

  "scripts": {
    "start": "npm run dev",
    "dev": "DEBUG=app:* babel-node -- server/server.dev.js",
    ...
  }

server.dev.js

import express from 'express';
import webpack from 'webpack';
import path from 'path';
import _debug from 'debug';

import routes from './routes';
import webpackDevMiddleware from './middleware/webpack-dev';
import webpackHotMiddleware from './middleware/webpack-hot';
import projectConfig from '../config';
import webpackConfig from '../webpack/dev.config.js';

const debug = _debug('app:server');
const app = express();
const compiler = webpack(webpackConfig);


app.use(express.static(path.resolve('src/assets')));

/* *******************
webpack configuration
******************* */

app.use(webpackDevMiddleware(compiler, webpackConfig.output.publicPath));
app.use(webpackHotMiddleware(compiler));


/* ******************
 ROUTES FOR OUR API
******************* */

routes(app);


/* ****************
 START THE SERVER
***************** */

app.listen(projectConfig.SERVER_PORT, () => {
  debug(`Express server listening on ${projectConfig.SERVER_PORT} in ${app.settings.env} mode`);
});

webpack/dev.config.js

import webpack from 'webpack';
import path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import _debug from 'debug';

import projectConfig, { paths } from '../config';

const debug = _debug('app:webpack:config:dev');
const srcDir = paths('src');
const assetsDir = paths('assets');
const nodeModulesDir = paths('nodeModules');

const deps = [
  'redux/dist/redux.min.js',
  'font-awesome/css/font-awesome.min.css',
  'slideout/dist/slideout.min.js',
];

debug('Create configuration.');
const config = {
  devtool: 'cheap-module-eval-source-map',
  entry: {
    app: [
      'react-hot-loader/patch',
      'webpack-hot-middleware/client?reload=true',
      paths('entryApp'),
    ],
    vendors: projectConfig.VENDOR_DEPENDENCIES,
  },
  output: {
    path: '/',
    filename: '[name]-[hash].js',
    publicPath: '/',
  },
  resolve: {
    alias: {},
    root: [srcDir],
    extensions: ['', '.js', '.jsx'],
  },
  module: {
    noParse: [],
    preLoaders: [
      {
        test: /\.js[x]?$/,
        loader: 'eslint',
        include: [srcDir],
      },
    ],
    loaders: [
      {
        test: /\.js[x]?$/,
        loader: 'babel',
        query: {
          cacheDirectory: true,
        },
        include: [srcDir],
      },
      {
        test: /\.json$/,
        loader: 'json',
      },
      {
        test: /\.css$/,
        loader: 'style!css!postcss',
      },
      {
        test: /\.(png|jpe?g)$/,
        loader: 'file?name=img/[name].[ext]',
      },
      {
        test: /\.(woff|woff2|eot|ttf|svg)(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'file?name=fonts/[name].[ext]',
      },
    ],
  },
  postcss: webpack => ([ // eslint-disable-line
    require('postcss-import')({ addDependencyTo: webpack }),
    require('postcss-url')(),
    require('postcss-cssnext')(),
  ]),
  plugins: [
    new HtmlWebpackPlugin({
      title: 'React Redux Boilerplate',
      hash: true,
      favicon: path.resolve(assetsDir, 'favicon.ico'),
      inject: 'body',
      template: path.resolve(srcDir, 'index.tpl.html'),
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin(),
    new webpack.optimize.CommonsChunkPlugin('vendors', '[name].[hash].js'),
    new webpack.DefinePlugin({
      __CLIENT__: projectConfig.__CLIENT__,
      __SERVER__: projectConfig.__SERVER__,
      __DEV__: projectConfig.__DEV__,
      __PROD__: projectConfig.__PROD__,
      __DEBUG__: projectConfig.__DEBUG__,
    }),
    new webpack.optimize.DedupePlugin(),
  ],
};

deps.forEach(dep => {
  const depPath = path.resolve(nodeModulesDir, dep);

  config.resolve.alias[dep.split(path.sep)[0]] = depPath;
  config.module.noParse.push(depPath);
});

export default config;

src/routes/index.js

import React from 'react';
import { Route, IndexRoute, Redirect } from 'react-router';

import AppLayout from 'layouts/AppLayout';
import Home from 'components/views/Home';
import About from 'components/views/About';
import Hello from 'components/views/Hello';
import Counter from 'components/views/Counter';
import Tools from 'components/views/Tools';
import Tool from 'components/views/Tools/Tool';
import NotFound from 'components/views/NotFound';

export default (
  <Route path="/" component={AppLayout}>
    <IndexRoute component={Home} />
    <Route path="home" component={Home} />
    <Route path="hello" component={Hello} />
    <Route path="about" component={About} />
    <Route path="counter" component={Counter} />
    <Route path="tools" component={Tools} />
    <Route path="tool/:id/:slug" component={Tool} />
    <Route path="404" component={NotFound} />
    <Redirect from="*" to="404" />
  </Route>
);

src/index.js

import { AppContainer } from 'react-hot-loader';
import React from 'react';
import ReactDOM from 'react-dom';
import { hashHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';

import configureStore from './redux/store';

import Root from 'containers/Root';
import routes from 'routes';

// *** STYLES *** //
// Path to svg logos icons
import 'assets/vendors/icons.svg.css';
import 'styles/app.css';

const rootEl = document.getElementById('root');
const store = configureStore();
// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(hashHistory, store);

ReactDOM.render(
  <AppContainer>
    <Root history={history} store={store} routes={routes} />
  </AppContainer>,
  rootEl
);

if (module.hot) {
  module.hot.accept('containers/Root', () => {
    // If you use Webpack 2 in ES modules mode, you can
    // use <Root /> here rather than require() a <NextRoot />.
    const NextRoot = require('./containers/Root').default;
    ReactDOM.render(
      <AppContainer>
         <NextRoot history={history} store={store} routes={routes} />
      </AppContainer>,
      rootEl
    );
  });
}

src/containers/Root.js

let Root;

if (__DEV__) {
  Root = require('./RootDev').default;
} else {
  Root = require('./RootProd').default;
}

export default Root;

src/containers/RootDev

import React, { PropTypes } from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router';

import DevTools from './DevTools';

const propTypes = {
  history: PropTypes.object.isRequired,
  store: PropTypes.object.isRequired,
  routes: PropTypes.object.isRequired,
};

function RootDev(props) {
  const { history, store, routes } = props;
  const showDevTools = () => (__DEBUG__ ? (<DevTools />) : false);

  return (
    <Provider store={store}>
      <div className="app">
        <Router history={history} routes={routes} />
        { showDevTools() }
      </div>
    </Provider>
  );
}

RootDev.propTypes = propTypes;

export default RootDev;

Thanks for your help

Sorry, copy-pasting and setting up a project is really tedious to validate bug reports 馃槃
Can you please provide this on GitHub?

Yes I understand :D this is my repo react-redux-boilerplate

I assume you won't need access to the commit history - I created an orphan repo and changed a few things because the other repo I'm working on is private. Here: react-hot-loader-attempt

Webpack configuration and dev server deployment all takes place inside the gulpfile.js between lines 149 and 254.

I'm also getting a strange websocket error when I run a new copy of the project in a different directory:

WebSocket connection to 'ws://localhost:8079/sockjs-node/633/rfhkw1kn/websocket' failed:
Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but
no response was received

I don't think this necessarily has anything to do with React Hot Loader; in fact, the hot reloading that was working (when editing the root component) still works, so I think it's safe to ignore that error for now. But if anyone knows why that is happening please let me know!

Thanks for the repro cases. I鈥檒l have a look in a week or two.

OK thanks for your time

@gaeron have you had a chance to look yet?

@benwiley4000 Not yet!

So I'm running into some similar issues, and it seems that this may all be related to stateless components tied to routes. Whenever I assign a react-router Route component to a component without any state, that route and all its children do not hot reload. Not really sure what the root cause is (I'm a long time Emberist just getting familiar with React), but I imagine that may put you in the right direction.

Thanks for all y'all's awesome work!

@davidlormor are you sure this is the problem - have you tried using state with your components to see if that fixes the issue? I'm only asking because I seem to recall experiencing the same issue without React Router in the way (hence my update in the original post), but I also didn't scientifically document my whole trial/error process.

@benwiley4000 yes, whenever I switch the Route's component from a stateless function to a class-based component the hot reload works, but when I switch it back to a stateless functional component the hot reload fails. (Tried across several routes/components)...it may have nothing to do with the Router, but rather something about stateless components, perhaps...like I mentioned I haven't dived too deep into this issue, just sharing my observations.

@gaearon I understand you're quite busy. I'd love for you to take another look at this one, if you can. I've been going without hot reloading for the last six weeks and I'd really like to get it working! :smiley:

@benwiley4000 try module.hot.accept your routes to cause a reload within your app.js. I had to do this for my case, perhaps it will work for you? Here is my working version: https://github.com/ctrlplusb/react-universally

@davidlormor I think it is react-router. I'm dealing with a similar issue. Checkout the following...

hmr fails

<Route component={()=><p>Hello</p>}

hmr works

class SimpleComponent extends Component {
 render(){
  return <p>Hello</p>
 }
}
<Route component={SimpleComponent} />

hmr also works

class SimpleComponent extends Component {
 render(){
 return <SimpleStatelessComponent />
 }
}

function SimpleStatelessComponent(){
 return <p>Hello</p>
}
<Route component={SimpleComponent} />

@l2silver I tried as hard as I could to reproduce the behavior you claimed worked for you, but I could not. I switched my route components to class components and I still could not get HMR to work.

I'm getting the error (as mentioned before): The following modules couldn't be hot updated: (They would need a full reload!).

@ctrlplusb To be honest I don't understand how that would solve the problem at hand. Still I was curious. This was my code before:

if (module.hot) {
  module.hot.accept('./root', () => {
    const NextRoot = require('./root').default;
    render(
      <AppContainer>
        <NextRoot {...{ store, history, onUpdate, routes }} />
      </AppContainer>,
      document.getElementById('app')
    );
  });
}

And I changed it to:

if (module.hot) {
  module.hot.accept(['./root', './route'], () => {
    // ...
  });
}

There was a difference - I no longer get the above warning, but still, nothing reloads. :disappointed: I also tried accepting only './routes' and I had the same result. What was your before / after?

@gaearon I would really, really, really appreciate you taking a look at the sample repo I provided you. Thanks. :)

hi guys,how's it going?
my component extends another component like this
class InnerQuery extends DelayRenderComponent
get the same error
the following modules couldn't be hot updated: (They would need a full reload!).

@starInEcust The same problem. If inner component extends any non react Component, hot reload willn't work - page will be full reloaded

It's major bug (component's hierarchy is usefull functionality), please, fix it

@starInEcust: can you open a separate issue for extending other components? A project reproducing the issue would be extremely helpful, as well.

@Kinjeiro: we are all working on this in our spare time, telling maintainers to "fix it" is unhelpful.

@starInEcust @Kinjeiro AFAIK, there's no mention in the React docs about inheritance, you may be attempting to use an unsupported workflow. They advocate "composition over inheritance".

@calesce I'm sorry for my tone, thank you for this wonderful plugin

@vdh thank you for idea. Need think about litle refactoring =)

@gaearon I spent a very long time tonight debugging this issue further to land on precisely what my problem is. It turns out, I was correct the first time, and this is a React Router issue. And in fact it's the same issue you have already identified here and here.

I was led to believe it was merely an import issue, but it turns out that imported components hot swap fine if they are included outside of a Router instance. I have also found that hot swapping works fine with React Router if everything the Router instance uses is defined in the same file as that Router instance. The Router itself can live in its own file and be imported to the root, but all Route instances and component definitions must live in the same file as Router, and hot reloading works. Obviously not practical for any real application.

I do have a follow-up question but it pertains to React Router so it's in this thread.

Glad you found out what was going on, @benwiley4000. Sorry none of us got back to you. It's clear that we need to document the known issues with RR 2/3, right now the solutions/workarounds are scattered in other issue threads. I'm going to close this one since we're tracking this in #249.

I think you can try like this. You should use 'hot' to wrap your component, then it will works.

`
import React from 'react'
import { hot } from 'react-hot-loader/root'
import './Home.less'

class Home extends React.Component {
state = {}

render () {
    return (
      <div className="home-page">
            This is Home Page.
      </div>
    )
}

}

export default hot(Home)
`

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ghost picture ghost  路  3Comments

lemonmade picture lemonmade  路  3Comments

mtscout6 picture mtscout6  路  3Comments

tiberiumaxim picture tiberiumaxim  路  4Comments

mqklin picture mqklin  路  3Comments