gaearon/react-hot-loader

Performance oriented .babelrc for TS users

theKashey opened this issue Β· 33 comments

We have to include example with babel configuration for TS users.
The key ideas:

  1. Add syntax parsers
  2. Add react-hot-loader transformation plugin
  3. Do not add any other transformation plugins to maintain build speed.

I would suggest to use tsconfig.json with module option set to esnext, then have babel to transform modules with transform-es2015-modules-commonjs. Thus the user can take the advantages of allowSyntheticDefaultImports and babel interop require, so that they don't have to use import wildcard everywhere.

Does anyone know if I can target TS to ES5 directly with react-hot-loader? I see in examples TS targets ES2015 which then will be transformed to ES5 with babel-loader.

No. In this case RHL will not see any Components, ie classes, as long they will be transpile, and you will lose ability to change like a anything except β€˜render’ method.

Hey all!

Maintainer of ts-loader here. I've just been checking out v4 of this mighty fine project and discovered that with v4 I need to start using babel:

When using TypeScript, Babel is not required, but React Hot Loader will not work without it.

With v3 I didn't need to which I liked. Would you be able to clarify why babel is needed with v4 please? If it's the only game in town that's cool - but if there's a way I can stick with just my TypeScript + core-js flow I'd love to.

If babel has to be in the mix I want to keep it as targeted as possible. I maintain a react-hot-loader example in the ts-loader repo so I'd like to provide a good boilerplate for people wondering how to use ts-loader with react-hot-loader.

I'd love to understand the limitation if you're up for explaining! 🌻

@johnnyreilly - so here is a fairy tale.
v4 was started to support arrow functions as class members, for the projects which does not use es5 transpilation in dev mode. Actually, result is a bit more debuggable, as long it is almost the "source" code.
So, if your component got an arrow-function member, that member will "lock" this, and we could not use proxy as we do in v3 to do RHL's work - there is no way to fake this for arrow functions.

As result - RHL will search for classes, and inject magic method, to EVAL something in the class scope and class context

And later we drop thousand lines from proxy, and just rely on this 3 lines long method.

If you will transpile TS to ES5 - RHL will not found any "class" it is looking for.
If you will not apply babel-plugin - RHL will be unable to update non-prototype-based method. Ie "onClick" handler.

This means - if you will transpile TS to ES5 or dont apply babel - RHL will still work (dont forget to import it before react), but just will lose some abilities. Not the main ones.

Hey @theKashey!

Thanks for that; super interesting context. I've a bunch of questions off the back of what you've said:

  1. So I can live without Babel, but from what you've said it sounds like I'll lose the ability to debug arrow functions in components. eg. onClick etc. That's a bit of a bind (see what I did? JavaScript jokes πŸ˜„ ) as I debug those a lot.

  2. If I was emitting classes from my TypeScript (say having an emit target of es2015 instead of es6) would I be fine without Babel? I'm not in that position now because of IE 11 but that day will one day come.

  3. What's the minimum Babel usage I have to have in place? My guess is that I need to have TypeScript emitting es2015 and that's me done. Is that right?

  4. I didn't realize import order was significant when it came to RHL. Can you tell me more about this please?

  5. As far as I can tell RHL v3 allows me to successfully hot module reload without babel in the mix. Debugging arrow functions seems fine as well. That being the case, what's the advantage of v4 over v3? Is it essentially down to this:

And later we drop thousand lines from proxy, and just rely on this 3 lines long method.

Trying to do a cost / benefit on:

  • living with v3 and keeping Babel out of the pipeline

vs

  • latest and greatest RHL but with Babel so slower build.

So I can live without Babel, but from what you've said it sounds like I'll lose the ability to debug arrow functions in components. eg. onClick etc. That's a bit of a bind (see what I did? JavaScript jokes πŸ˜„ ) as I debug those a lot.

Not debugging. Replacing. RHL will lose ability to repeat changes in the new constructor and will not repeat changes you made.

If I was emitting classes from my TypeScript (say having an emit target of es2015 instead of es6) would I be fine without Babel? I'm not in that position now because of IE 11 but that day will one day come.

If you dont use babel plugin - the "mode" you are using does not make any sence. No babel - no cry.
And, yet again, RHL is for dev mode. Are you developing in IE11?

What's the minimum Babel usage I have to have in place? My guess is that I need to have TypeScript emitting es2015 and that's me done. Is that right?

Only RHL patch is enough. In dev mode.

I didn't realize import order was significant when it came to RHL. Can you tell me more about this please?

React-hot-loader should patch React before any component would be created. Import order is not significant, but better to import RHL before anything else.

As far as I can tell RHL v3 allows me to successfully hot module reload without babel in the mix. Debugging arrow functions seems fine as well. That being the case, what's the advantage of v4 over v3? Is it essentially down to this:

v4 could handle almost any code you may write, v3 could handle almost no code I wrote.

latest and greatest RHL but with Babel so slower build.

That's why I created this issue - how to setup RHL and TS and babel without any significant slowdown.

Here is the minimal babel config I ended up with:

{
  loader: 'babel-loader',
  options: {
    plugins: [
      '@babel/plugin-syntax-typescript',
      '@babel/plugin-syntax-decorators',
      '@babel/plugin-syntax-jsx',
      'react-hot-loader/babel',
    ],
  },
}

I put it BEFORE ts-loader. Works fine for me but the build is noticeably slower.

Actually we could try to make it better to TS users, and NOT require nor babel, nor compiling to ES6 instead of ES5.
The only thing one need - set a custom property on each "spotted" class, to wrap .bind with some custom hook (like React-Hot-Loader v1-2-3 did, actually), everything else ProxyComponent can handle.

That may make TS users more happier, or just make it possible to use RHL, as long most of them have a target: es5, and, as result, could not use RHL :(

The only thing one need - set a custom property on each "spotted" class, to wrap .bind with some custom hook (like React-Hot-Loader v1-2-3 did, actually), everything else ProxyComponent can handle.

@theKashey could you go into more details on this? I would be happy to remove babel from my pipeline and node_modules

There is nothing you could do, but there is something that could be changed in the RHL's internals.
Thats is not an easy task, and we specially did not do it, as long it... you know.. hacks :)

There is nothing you could do, but there is something that could be changed in the RHL's internals.
Thats is not an easy task, and we specially did not do it, as long it... you know.. hacks :)

@theKashey I would ❀️ that to happen!

Question on this:

The only thing one need - set a custom property on each "spotted" class, to wrap .bind with some custom hook (like React-Hot-Loader v1-2-3 did, actually), everything else ProxyComponent can handle.

Would that work for arrow functions as class members? eg

export class SomeClass extends React.Component<IProps, IState> {

    // arrow function as class member; shorter than using `bind` in the constructor
    anArrowFunctionThatsWhatIAm = (event: React.MouseEvent<HTMLButtonElement>) => {
        event.preventDefault();

        // do stuff
    };

    render() {
      // ...
    }
}

No. Only babel magic can handle arrow functions.
Yes. You are transpiling TS into ES5, and where arrow functions does not exists.

That's a shame :-(

Unfortunately nothing could change context of arrow function, is it bound to the single this, and nothing could change it.
That means - to create an arrow function that this should execute it. We are using eval for it.
The simple solution might look like

function renegenerateArrowFunction(code){
  return eval(code);
}
instance.arrowMember =  renegenerateArrowFunction.call(instance, newFunctionBody)

But arrow function might "consume" something from variable scope, and we have to preserve that scope.
As result - we need a babel plugin to find class and add a new method with eval inside :(

Nor Proxy, nor Reflection API could not help here :(

Can we just use babel to transpile TypeScript, and do type checking with something else asynchronously?

Doesn't look like a good idea, just didn't think of the limitations transpiling TypeScript using babel.

What about using typescript plugins to repeat the things babel do?
https://github.com/Microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin

Seems that TypeScript plugins can't do it (at least right now):

image

UPD: but it seems to be possible with Custom Transformers which I was not aware of but seems ts-loader supports it.

I think @johnnyreilly can tell more about this

I was getting a hell lot of TS errors with the preferred config from README because with it the code is compiled first by Babel, and by TypeScript after, and TypeScript compiler goes nuts on 'babelified' code.

Working config for me (you must put babel-loader before ts-loader in config, but, due to webpack's reversed loaders order, it gets executed after):

      {
        test: /\.ts|\.tsx$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              plugins: [
                '@babel/plugin-syntax-typescript',
                '@babel/plugin-syntax-decorators',
                '@babel/plugin-syntax-jsx',
                'react-hot-loader/babel',
              ],
            },
          },
          {
            loader: 'ts-loader',
          },
        ],
      },

May be you have .babelrc and it adds some transformations you are not expecting?
Add β€˜babelrc: false’ to loader option and place it after ts.

Otherwise RHL will not properly work unless you are set es6 as a target.

@theKashey Nope, I have no .babelrc and am getting errors like this:

ERROR in /.../components/ClientListItem.tsx
./client/components/ClientListItem.tsx
[tsl] ERROR in /.../components/ClientListItem.tsx(63,36)
      TS7006: Parameter 'key' implicitly has an 'any' type.

ERROR in /.../components/ClientListItem.tsx
./client/components/ClientListItem.tsx
[tsl] ERROR in /.../components/ClientListItem.tsx(63,41)
      TS7006: Parameter 'code' implicitly has an 'any' type.

I assume that type checking happens after Babel compiles the code as I see no such lines in ClientListItem.tsx and VS Code doesn't output such errors. It looks like transpiled key prop gets checked (which makes sense), not sure about the code though.

πŸ˜†πŸ˜†πŸ˜†πŸ˜•β˜ΉοΈπŸ˜ŸπŸ˜£πŸ˜–
Sure it is producing TS incompatible code. "We" just don't have linting enabled on build step(a common way to speed up builds).
Ways to fix:

  • change readme to show how split linting and compiling, thus make babel-plug work "without" errors.
  • add react-hot-loader/babel-ts which will emit code with type signatures. I am not 100% sure that babel will allow me to do it. babel 7 - yes, but not sure about 6.
  • propose ts->es6->babel then, to ship es5 into the production, one have to override config file location)
  • add some unsafe proxy magic, to hack this provided to bound handlers in "constructed" component, and redirect read/writes to the "real" this.
  • don't use babel plugin at all. We could restore old webpack loader to "register" something and provide some ground truth to reconciler, but bound method overload will not work anyway.

PS: It is easy to make .bind methods works, but, as long nowadays everybody uses arrow function, and they are transpiled "into" the constructor - we could not get them out without eval :(

Reopening task.

A hacky, but possibly easy to implement way of handling this problem is using //@ts-ignore to suppress the warnings.

i.e.

// @ts-ignore
__reactstandin__regenerateByEval(key, code) {
// @ts-ignore
    this[key] = eval(code);
}

I've never found a good source of documentation for //@ts-ignore, but the announcement blog post skims over it:

These comments are a light-weight way to suppress any error that occurs on the next line.

Unfortunately you can't block specific errors at the moment, only all errors.

Ok. PR just opened.

  • // ts-ignore by @AndyCJ works perfectly! But only for babel-7
  • I also updated solution for babel6/babel after TS. As long ts-loader/awesome-loader support config override, and configs support "extend" by design - separating dev and prod TS conf, ie ES5 and ES6 - should not be a problem.

Please review.

OK for me, but I think another look from a TS expert would be great.

I don't know anything about babel, so I can't comment on that.

Thanks for updating the example project to show using babel 7. I struggled a little on the weekend with the differences between the documentation showing babel 7, and the example using babel 6, before hitting the "invalid" typescript issue.

I got there in the end by guessing which packages I needed to grab/update, so having that in the example should make things smoother for others who haven't had much exposure to babel.

You guys are very active and responsive on this project. It doesn't mean much, but I'm personally very impressed!

Thank you for all your time and effort.

@Madou - are you TS expert?

Meanwhile - found one more way to "babel" TS - it is build into awesome-typescript-loader!
https://github.com/s-panferov/awesome-typescript-loader#usebabel-boolean-defaultfalse

     loader: "awesome-typescript-loader",
            options: {
              silent: process.argv.indexOf("--json") !== -1,
              useBabel: true,
              babelOptions: {
                plugins: ['react-hot-loader/babel']
              }
            }

Released in v4.1.3

DDzia commented

@theKashey, do you can provide your configs(webpack.config.js, tsconfig.json, package.json(for versioni))?

Our example - https://github.com/gaearon/react-hot-loader/tree/master/examples/typescript - uses babel 7 under the hood, and might be not the best option.
The easiest way right now - ts -> es6 -> babel(with RHL plugin only) -> js(or es6)
All settings in all variants you might found in README.

@theKashey Is there a way to use it without babel now? Both ts-loader and babel are compiler. I believe most people think that ts-loader and babel are redundant at the same time, and many people are forced to do so.

It will be never possible to use all the features without babel. With webpack-loaders restored RHL will be almost usable for TS-only applications.