React-hot-loader: Full reload - Typescript + Babel

Created on 24 Mar 2017  路  20Comments  路  Source: gaearon/react-hot-loader

I have a very minimal setup with Webpack, tslint, Typescript and Babel.

On every save I get the output:

Cannot apply update. Need to do a full reload!
(anonymous) @ app.js:15223
app.js:15224 [HMR] Error: Aborted because 165 is not accepted
Update propagation: 165 -> 431

I noticed that the module.hot.accept callback is never invoked.
I have followed the migration guide closely.

Running the dev server with: webpack-dev-server --hot.

Any hints?

app.tsx:

import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";

import App from "./components/app/App";

function startApp() {
  ReactDOM.render(
    <AppContainer>
      <App />
    </AppContainer>,
    document.getElementById("app-root"),
  );
}
startApp();

if (module.hot) {
  module.hot.accept("./components/app/App", () => { alert("accept"); startApp(); } );
}

App.tsx:

import React from "react";

export default class App extends React.Component<void, void> {
  public render() {
    return (
      <div>NO</div>
    );
  }
}

babelrc:

{
  "presets": [
    ["es2015", {"loose": true, "modules": false}],
    "stage-2"
  ],
  "plugins": ["react-hot-loader/babel"]
}

webpack.config.js:

const path = require("path");

const config = {
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".jsx"],
    alias: {
      pos: path.resolve(__dirname, "src"),
      cmp: path.resolve(__dirname, "src/components"),
      assets: path.resolve(__dirname, "assets"),
    },
  },

  entry: {
    app: [
      "react-hot-loader/patch",
      "./src/app",
    ],
  },

  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
  },

  module: {
    rules: [
      // tslint linting.
      { test: /\.tsx?$/, enforce: "pre", use: "tslint-loader" },
      // Typescript files.
      {
        test: /.tsx?$/,
        use: ["babel-loader", "ts-loader"],
        exclude: /node_modules/
      },
      // Images.
      {
        test: /\.(jpe?g|png|gif|svg)$/i,
        use: [
          {
            loader: "file-loader",
            options: {
              name: "[name]_[hash].[ext]",
              publicPath: "/img/",
              outputPath: "img/",
            },
          },
        ],
      },
    ],
  },

  devServer: {
    contentBase: path.join(__dirname, "dist"),
    port: 9000,
    historyApiFallback: true,
  },
};
module.exports = config;
bug

Most helpful comment

My local, minimal boilerplate contains a few visible changes from your current setup. I pulled config settings from the @next's babel migration recommendations. Outside of that and of note, I use awesome-typescript-loader.

package.json

  "devDependencies": {
    "awesome-typescript-loader": "3.1.2",
    "babel-core": "^6.24.0",
    "babel-loader": "6.4.1",
    "babel-preset-es2015": "^6.24.0",
    "babel-preset-react": "^6.23.0",
    "babel-preset-stage-0": "^6.22.0",
    "react-hot-loader": "next",
    "typescript": "2.2.2",
    "webpack": "^2.3.2",
    "webpack-dev-server": "^2.4.2"
  },

webpack.config.js

module.exports = {
  entry: [
      'babel-polyfill',
      'react-hot-loader/patch',
      './src/index.tsx',
  ],

...

module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          'react-hot-loader/webpack', 'babel-loader', 'awesome-typescript-loader'
        ],
        exclude: /node_modules/,
      },
    ],
  }

tsconfig.json

{
    "compilerOptions": {
        "module": "es6",
        "target": "es6",
        "lib": ["es6", "dom"],
        "moduleResolution": "node",
        "sourceMap": true,
        "jsx": "preserve",
        "rootDir": "src",
        "allowSyntheticDefaultImports": true
    },
    "exclude": [
        "node_modules"
    ]
}

.babelrc

{
  "presets": [["es2015", { "modules": false }], "stage-0", "react"],
  "plugins": ["react-hot-loader/babel"]
}

All 20 comments

PS: I've also tried to disable the tslint-loader.
No effect.

Hi,

Just checking in with the exact same thing happening :(

My local, minimal boilerplate contains a few visible changes from your current setup. I pulled config settings from the @next's babel migration recommendations. Outside of that and of note, I use awesome-typescript-loader.

package.json

  "devDependencies": {
    "awesome-typescript-loader": "3.1.2",
    "babel-core": "^6.24.0",
    "babel-loader": "6.4.1",
    "babel-preset-es2015": "^6.24.0",
    "babel-preset-react": "^6.23.0",
    "babel-preset-stage-0": "^6.22.0",
    "react-hot-loader": "next",
    "typescript": "2.2.2",
    "webpack": "^2.3.2",
    "webpack-dev-server": "^2.4.2"
  },

webpack.config.js

module.exports = {
  entry: [
      'babel-polyfill',
      'react-hot-loader/patch',
      './src/index.tsx',
  ],

...

module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          'react-hot-loader/webpack', 'babel-loader', 'awesome-typescript-loader'
        ],
        exclude: /node_modules/,
      },
    ],
  }

tsconfig.json

{
    "compilerOptions": {
        "module": "es6",
        "target": "es6",
        "lib": ["es6", "dom"],
        "moduleResolution": "node",
        "sourceMap": true,
        "jsx": "preserve",
        "rootDir": "src",
        "allowSyntheticDefaultImports": true
    },
    "exclude": [
        "node_modules"
    ]
}

.babelrc

{
  "presets": [["es2015", { "modules": false }], "stage-0", "react"],
  "plugins": ["react-hot-loader/babel"]
}

@seamc so this setup is working for you?

@seamc thank u for the demo, do u think this problem can be solved without babel?

@theduke I don't no why, but you must explicitly require new App component and put it into startApp argument in accept callback. I've fixed your example. Check it out, please.

import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";

import App from "./components/app/App";

function startApp(App) {
  ReactDOM.render(
    <AppContainer>
      <App />
    </AppContainer>,
    document.getElementById("app-root"),
  );
}
startApp(App);

if (module.hot) {
  module.hot.accept("./components/app/App", () => { alert("accept"); startApp(require("./components/app/App").default); } );
}

I had the same problem. So I took react-hot-boilerplate and transformed it into typescript with minimal changes. Then hot-reloading stopped working anymore. Everything was fine in console, but changes weren't happening on page actually.
image
Then I fixed the issue like this. But I really don't understand roots of the problem. Does anyone know?

Further to my previous comment I found the issue. In case of Typescript __REACT_HOT_LOADER__.register isn't invoked for default. I've compared result bundles of equivalent TS and JS sources:
image
It looks like a bug in react-hot-loader.

I added a test to reproduce the issue.

@seamc way is working

@dizel3d you saved my day! i don't know why it works, and i'm also wondering why the others don't need it indeed...

@dizel3d
It is because of typescript and the default export. If you export the App without default and import it like this, it works without the require and .default :

import {App} from "./components/app/App";

One general workround:

if (module.hot) {
      // accept all changes, evaluate the whole js file
      module.hot.accept();
      render(Root);
}

related https://github.com/gaearon/react-hot-loader/issues/413

React Hot Loader v4 provides a good TypeScript support using Babel. I added an example with Typescript.

I just had this problem, here's what I ad to use to fix it:

const mount = document.getElementById('mount');

const render = (Component: any) => {
  ReactDOM.render(<Component />, mount);
}

render(App);

if (module.hot) {
  module.hot.accept('./App', () => {
    render(require('./App').default); // the .default is important
  });
}

I found that just doing render(require('./App')); resulted in a full page reload, but doing render(require('./App').default); gave me the hot-reloading goodness I was expecting!

I experienced a similar issue where the hot reloading worked the first time after I started the dev server but every subsequent change triggered a full reload since the root component (index.tsx) wasn't accepted. When I switched to awesome-typescript-loader instead of ts-loader it started working with the exact same config.

If you implement one of the fixes above and are still seeing full reloads React Router V3 may be the cause.

I noticed the same issue as https://github.com/gaearon/react-hot-loader/issues/525#issuecomment-300247983 - default exports are NOT registered (most of the time?) and because of that hot reloading for those components does not work.
So this does not work:

import * as React from 'react';
import { withStyles, WithStyles } from 'material-ui/styles';

const styles = {...};

export const MyComponent: React.SFC<WithStyles<'title'>> = () => <h1 className={classes.title}>Hello world!</h1>;

export default withStyles(styles)(MyComponent);

However this one works:

const MyStyledComponent = withStyles(styles)(MyComponent);
export default MyStyledComponent ;

Maybe someone could a loader or something that detects those kind of e default export where an expression is used and rewrite them so they use a temporary const?

EDIT: Also the tag "fixed in next" is wrong - I just tried upgrading and can confirm it does NOT work:

var styles = function styles(theme) {
  return {...};
};
exports.default = _styles_1.withStyles(styles)(exports.AppLayout); 
(function () {
  var reactHotLoader = __webpack_require__("./node_modules/react-hot-loader/patch.js").default;
  var leaveModule = __webpack_require__("./node_modules/react-hot-loader/patch.js").leaveModule;
  if (!reactHotLoader) {
    return;
  }
  reactHotLoader.register(styles, "styles", "D:\\dev\\web\\haushalt-tracker\\src\\components\\core\\AppLayout.tsx");
  leaveModule(module);
})();

The default export is not registered.

EDIT 2:
Ok this is interesting - even though no register call for the default export is generated, hot reloading seems to work. So I guess switching to react-hot-loader@next solves this problem.

EDIT 3:
Ok - it does not work correctly - it just does not display a warning any more it seems.
When I try the fixed version, my component is correctly updated while keeping it's state.
If I try it without the const fix with @next version, it kills the state (which it does not otherwise).

So this issue needs to be fixed in v3 AND v4.

Just in case someone has the same issue I was having. If you target ES6 with Typescript then apply the hot reload loader it won't work as if messes with the scope of the classes, not sure on the underlying issue.

loaders: [
    "react-hot-loader/webpack",
        "ts-loader"
],

Does not work.

loaders: [
    "react-hot-loader/webpack",
    {
        loader: "babel-loader",
        query: {
            presets: [
                ['es2015', { 'modules': false }]
            ]
        }
    },
    "ts-loader"
],

Does work and retains the state.

Was this page helpful?
0 / 5 - 0 ratings