gaearon / react-hot-loader

Tweak React components in real time. (Deprecated: use Fast Refresh instead.)

Home Page:http://gaearon.github.io/react-hot-loader/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Element type comparing

Mistereo opened this issue · comments

It seems like with react-hot-loader@3 it's not possible to check element type for imported components:

const element = <ImportedComponent />
console.log(element.type === ImportedComponent)

Logs false for me.
Is this a bug or something that should not be supported?

Hi @Mistereo

This is intended, react-hot-loader@3 patches React.createElement(<ImportedComponent /> is equivalent to React.createElement(ImportedComponent)) so that it returns an element of a proxy wrapper for your components instead of the original component, this is part of allows to replace methods on the component without unmounting.

Do you have any need for <ImportedComponent />.type === ImportedComponent to be true?

@nfcampos I'm trying to implement conditional rendering depending on children types.
I can just set some static prop on these components as a workaround, and check this prop instead.
Just wondering if this can be avoided.

@Mistereo no, I don't think it can be avoided with the current architecture

I'm running into the same problem, this is how I am working around the issue:

const ImportedComponent = (props) => { ... }
const ImportedComponentType = (<ImportedComponent />).type;

const element = <ImportedComponent />
console.log(element.type === ImportedComponentType)

this is definitely a big flaw ! not able to check child type of which class ...

Is it possible to fix this thing or is it too big of a change?

Ran into this today.

One option is to check using .type.name like this:

import Component from /* ... */;
// Check like this
element.type.name === Component.name

However, even this would also break once the resulting code was minified.

So for now I'm using @JorgenEvens 's workaround, thanks for that.

I'm using another approach

const ImportedComponent = (props) => { ... }
ImportedComponent.isMyImportedComponent = true;

// Check like this
element.type.isMyImportedComponent === true

Just ran into this myself and I'm pretty stumped. I'd love to do something a little more elegant than checking displayName or cramming my components with static flags, but I can't seem to figure out a solution. Is it really hopeless, @nfcampos ?

commented

@gaearon Just want to check beforehand that is it possible to fix this in a systematical way or not? If yes, I'll try to dig into it and fix. Thanks.

also posted on react-proxy gaearon/react-proxy#80

+1
Same problem here, for now I'm using this temporary solution:

const Component = (props) => { ... }
Component.type = (<Component />).type

...
Children.map(children, child => {
  if (child.type === Component.type) {
    ...
  }
})
commented

Based on what's said here, I've build a very little Babel plugin which transforms foo.type === MyComponent in foo.type === (<MyComponent />).type.

Save it as a .js file and reference it in your babel options in dev mode.

module.exports = function (babel) {
  var t = babel.types;
  return {
    visitor: {
      BinaryExpression(path) {
        if (
          (path.node.operator === '===')
          && (t.isMemberExpression(path.node.left))
          && (t.isIdentifier(path.node.left.property))
          && (path.node.left.property.name === 'type')
          && (t.isIdentifier(path.node.right))
        ) {
          var className = t.stringLiteral(path.node.right.name).value
          var newExpr = t.memberExpression(t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier(className), [], true), null, [], true), t.identifier('type'))
          var rightPath = path.get('right')
          rightPath.replaceWith(newExpr)
        }
      }
    }
  }
}

@lgra - it will not always work, and even can break some generic code. The problem is that React-Hot-Loader will always somehow wrap original class with something to.. just do his work.
And that somehow is the internals you should not rely on. It might be changed at any point.

Currently, there are 4 options:

  1. Trick by @JorgenEvens. It will always work.
  2. We can ask react-proxy to expose the original class as @yesmeck propose in gaearon/react-proxy#68
  3. It is possible to make construction element.type instanceOf ImportedComponent work. Will you use it?
  4. We can wrap the original class just after creation, so comparisons will start working instantly. But it will introduce yet another magic code transform we are trying to avoid.

The best way, I can imagine - we can expose special function for comparison, but how to provide a requirement to use it?

commented

@theKashey hi Anton. My point of vue is to be less intrusive as possible. At my company, several projects share dozen of UI components. Every projects are not using the same version of REACT, webpack, Babel or even ReactHotLoader. The type comparison is often used in our components, and I don’t want to modify this share code as I can’t quantify the risk, nor the impact on production code - where this modification is not necessary.
This is why I’ve created this Babel plugin. It doe’s the @JorgenEvens trick on the fly at transpiration time. I’m applying it only to my shunk, and only in dev mode.
This allow me to use latest version of ReactHotLoader, without any change in my code nor colegue code, and without any impact on production code.
I’m pretty sure you’re true when you say it won’t fit any situation. I don’t give this as an universal solution. It’s just an other way to implement the trick, that can save time in some circontances.

@igra - can you show the rest of your babel plugin? To be more clear - newExpr.
Actually - code might be quite safe if

element.type === ImportedComponent

// will be transpiled into

compareRHL(element.type, ImportedComponent)

//where
compareRHL = (a,B) =>{try{ return a===B || (typeof React !== 'undefined' && a===<B/>.type)} catch(e){} return false; } 

Key points -> keep the old comparison, and there might be no React, and long something.type === something is quite generic.

commented

@theKashey - There's nothing else ! You've got here the whole code of the plugin. It doesn't produce final transpiled code. It produces JSX code that will be transformed by babel react preset (https://babeljs.io/docs/plugins/#plugin-preset-ordering).

// Things like:
element.type === ImportedComponent

// is transform to
element.type === <ImportedComponent />.type

In the Abstract Syntax Tree, the plugin matchs strict egality expressions (BinaryExpression where operator is ===), where left is the literal type property of any object (a MemberExpression where property is an Identifier of name type), and where right is an Identifier (assuming this identifier is a REACT class).
In the matched expression, it replaces the right path by the type member of an JSX element of the JSX identifier found as the right parts of the expression:

t.memberExpression( // an object member expression
  t.jSXElement( // the object is JSX element expression ...
    t.jSXOpeningElement( // ... where opening element ...
      t.jSXIdentifier(className), // ... is of class identified by the right parts of the egality - className = t.stringLiteral(path.node.right.name).value
      [], true // ... with no attributes nor closing element
    ),
    null, [], true // no children
  ),
  t.identifier('type') // the member is identified by the name 'type'
)

This is the same AST tree as if you write your code the @JorgenEvens way.

The limit of this plugin is that it's assuming that the right identifier is a REACT class identifier. If you don't use type for something else than REACT object instance type, this is not an issue. And event if you uses a custom type member for something else and want to compare it to something, invert the binary expression to make the plugin skip it.

But is far not easy to transpile something in node_modules and get all the things working properly.
Anyway - sooner or later we will solve this.

commented

No, transpiling node_modules is definitely not the way this should be solved (if it is solvable at all).

@gaearon - could React help? Expose an underlaying type from a proxy as .type prop?

In the same time I am just wondering - why one need this comparison outside the tests. What are you going to solve, to archive?

I mean - should the case be solved at all.

commented

@gaearon I've got rules in my Webpack config to apply it only to my own modules and git submodules - but you're true : what if a npm module need to be patch ? and for instance, I don't have any external npm modules presenting this pattern.

@theKashey We're using this pattern in some of our components.
Case 1: A father element will inject some specific logic to a child element if it's of a certain type. For example, a CustomRadioGroup element will compute a unique name and inject it to any CustomRadio child - but not to CustomLabel child.
Case 2: A father element will reflow its children depending on there kind. For example, CustomTable will have some CustomHeader, some CustomHeaderRow and some CustomRow children. CustomTable will render a HTML table tag, and put any CustomHeader in header tag, CustomHeaderRow in thead tag, and CustomRow in tbody. It could also parse the first CustomHeaderRow to generate colgroup and col tag accordingly.

@gaearon @theKashey Maybe it's not a good pattern. Maybe a specific static property of the class is a better way - it allows the definition of several classes that will behave the same way when put into a father (for example, CustomOddRow and CustomEvenRow are body rows - not so far than the concept of display: table-row). I'll think about a behaviors static array property.

I think it is not a good pattern but I think it is used by some people. React Hot Loader should not break it.

The problem is that with the current strategy (proxy) we do not have the choice.

Look like the best way to solve this issue (and not only this one) - it is move React-Hot-Loader from React, to React-dom. Solve it from inside.

As long all idea around RHL is to bypass Reconciler's element.type === existingChild.type, and we are trying to undo this inside the client code. Left arm is fighting with the right one.

Anyway - this task could be solved for ES5 classes, as long we have full access to the prototype and do all the magic we need, but I am not sure about ES6 as long classed are sealed a bit.

commented

@gaearon I agree with you. As long as a proxy is used, as type is a readonly property, and as it's used internally by REACT when cloning element, I'm pretty sure it's not solvable at all.

So - it task is close to doable.
It is possible(*) to always use the first instance of a class or a function.

  • in case of class we can monkey-patch the original prototype, and backport some changes via life cycle methods. As long constructor in es6 is immutable - it is the only way.
  • in case of stateless functional component, it is also "doable", but will require changes on the React side -> replace FunctionCall(params) by Function.call(null, params..). Next one can overload .call method of a function to hot-replace it.

So - it is doable, but I am not sure that one should try to solve this problem.

@lgra thanks very much — it may be brittle, but it’s better than adding nonsense code in prod — with a little adaptation this addresses it for me for now.

Maybe it would be worth to add a helper function which will do all the magic, based on the implementation of react-hot-loader?

Something like that:

import React from 'react';

export const isElementOfType = component => {

  // Trying to solve the problem with 'children: XXX.isRequired'
  // (https://github.com/gaearon/react-hot-loader/issues/710). This does not work for me :(
  const originalPropTypes = component.propTypes;
  component.propTypes = undefined;
  
  // Well known workaround
  const elementType = (<Component />).type;

  // Restore originalPropTypes
  component.propTypes = originalPropTypes;

  return element => element.type === elementType;
};

The usage:

isElementOfType(MyComponent)(<MyComponent />) // true
commented

@bathos You're welcome.
I have enhanced my babel plugin to make replacement only if the right parts of the comparison is an imported class, and log what it had replace. Here's the code:
https://github.com/lgra/lg-webpack/blob/master/scripts/babel-type-comparison.js

import {isComponentsEqual} from 'react-hot-loader'

isComponentsEqual(element.type, ImportedComponent);

// for production
const isComponentsEqual = (a, b) => a === b;

// for dev
const isComponentsEqual = (a, b) => a  === b || a === getProxyByType(b);

Some things is easier doable from inside.

@theKashey it is ok for my issue. =) It would be worth to add that to react-hot-loader.
P.S. I think you meant areComponentsEqual instead of isComponentsEqual.

It's a 0:55 o'clock. I am already not here a bit.

@theKashey , @gaearon can the function resolveType (https://github.com/gaearon/react-hot-loader/blob/master/src/patch.dev.js#L139) be exported as public API?

I would like to use it for type checking in the following manner (if I get it correctly, it should work):

// Helper (for 'dev' environment):
const isElementOfType = type => element => resolveType(type) === element.type

// Usage:
const isFoo = isElementOfType(Foo)
const isFoo(<Foo />) // true

Are there reasons not to export the resolveType as public?

The main reason - your code should not relay on RHL internal implementation. But there is no way :(
What I could propose:

  1. We are exporting resolveType, but you are not using it
  2. We could add @lgra's magic into our babel plugin, to fix the issue seamlessly for you.
  3. Is automagicaly does nothing for production code.

You will not change your code, but your code will work. This is not a great solution, as long it will not fix third party libraries, which may relay on type, but this is far better that nothing.

I agree about 3rd parties. :( But it is not good for RHL as devtool to have such "unresolvable issues" as this one. In the case the devtool has a big influence to production. I think it is something like a problem. :/ It was some lyrics :)

About my case. The code have no external dependencies with React components. There is a helper in the code like I described above and the only way to check the type of element is using this helper (the helper is already exist). There is the only place in the code to fix this issue.

So for me, I would use resolveType if you exported it, at least as temporary quick solution.

If you decided to expose @lgra's solution as public it would be nice. At least it would be good start point, I think.

Anyway, even for the case with babel magic - method have to be exposed.

@theKashey sounds nice) We could kill two birds with one stone :)

Nice to hear! =) Thanks guys!

@neoziro Thanks for the work on areComponentsEqual.

How should one solve the issue where a third party dependency does this check? If react-hot-loader is imported at all it causes those checks to fail. This is noted above, but I wasn't able to find a resolution in the ensuing conversations.

Thanks!

@mmoutenot good point! I don't have any solution yet. I think we will have to wait for React team to provide a Hot Reloading solution built in React.

Gotcha, it looks like some third party libraries are working to ensure things still work.

For example:
palantir/blueprint#723
palantir/blueprint#2095

I'll keep my eyes open for a solution from the React team.

@neoziro the current areComponentsEqual solution does not work with 3rd party libraries unless they change their code. Can this issue remain open until there's a better approach?

...this issue will remain open, but we don't have another soluting yet....

I don't know if babel can process 3rd party node_modules. If it can, we could try to write a plugin based on @lgra 's one which processes 3rd party modules using areComponentsEqual, I think.
It could be general solution. But there are performance issues in the case, I think.

It could. Even more - create-react-app already transpiling all the code, and RHL's babel plugin cound also safely extended to all the node_modules.
The problem - node_modules contain transpired JSX, and @lgra 's plugin will not work out of the box. And it much harder to create stable version, capable to work on transpiled, bundled, and uglified code.

Volunteers?

commented

Maybe it's already been told, but here is my idea of the day, based on my use case.

RHL is a huge value when creating our own code, or dealing with our own git submodules or npmised source packages, included as source JSX file. Using babel, it allows us to replace on the fly submodules file (for example css variable file) and insure our own code base to use common dependencies. The babel plugin I wrote perfectly fit this use case.

RHL is less value, even no value at all, when dealing with third parties transpiled npm modules.
Is it possible to make RHL proxified only our own classes, and let node_modules transpiled classes unchanged ?
A way to set what musn't be proxified could based on a vendor chunk defined in web pack config.

Not so fast :( We have no information about the origins of a class or file. We have some part of this information from registration, and they cover only "yours" code, but they dont see a lot components(HOC, decorated and so on).

The closest approach is to extend babel plugin to node_modules, and next handle classes from node_modules as something not going to be replaced.
Which is not always true, as long redux's connect should be handled.

The best approach for now - create a proposal to React team, to make thing more possible. If React will be more RHL friendly, RHL could be less aggressive to JS code.

commented

Understood.

Two questions:

There is no (easy) way to somehow tell RHL not to proxy certain components (using file path, component name, whatever)?

Also, there is no way to "unproxy" those components somehow (using some dirty trick or whatever)?

Hacks you said, @idubrov ?

import reactHotLoader from 'react-hot-loader';

const originalCreateElement = React.createElement;
React.createElement =  (type, ...args) => {
   if(type should not be replaced){
      const oldValue =  reactHotLoader.disableProxyCreation;
      reactHotLoader.disableProxyCreation = true;
      const element = originalCreateElement(type, ...args)
      reactHotLoader.disableProxyCreation = oldValue;
      return element;
   } else {
          return originalCreateElement(type, ...args);
   }
};

I tried doing something like this, but I wasn't able to get the original React.createElement -- it's replaced before I can grab it. I think, it's because of react-hot-loader babel plugin, which adds import to react-hot-loader, which in turn calls ReactHotLoader.patch(React), which replaces React.createElement. I'm too late to the party!

You are right. RHL tries to hack React as soon as possible, and the only way to disable it - use reactHotLoader.disableProxyCreation. Which is a bit tricky flag, as long it will disable creation, but if something already got corresponding proxy - it will be returned.

Actually, that seems to do the the trick. I only use GraphiQL component in one place, so wrapping the place it is used in the following way:

ReactHotLoader.disableProxyCreation = true;
const result = (<GraphiQL>...</GraphiQL>);
ReactHotLoader.disableProxyCreation = false;
return result;

seems to solve my issue.

Thanks!

Would it be possible to have "nothot" function to mark components not to be proxable?

Like,

import { nothot } from "react-hot-loader";
import GraphiQL as GQL from "graphiql";

const GraphiQL = nothot(GQL);

...

Ok, so lets extract my latest proposal to completely nothot_ node_modules into the separate issue #991

Hi! Is proxy creation required for the correct working of RHL?
What if we'll always set disableProxyCreation to true?
Also, what if create originalType property for type comparison instead of areComponentsEqual and <Compoennt>.type?

RHL could not work without Proxies. Proxies are RHL.
But you can use cold API to disable proxy creation for any specific component.

@theKashey What do you think about creating originalType property (inside RHL) for type comparison instead of areComponentsEqual and <Compoennt>.type?

Then it would not work without RHL, in production, for example.

Of course, but in this case, we can implement the simplest implementation of areComponentsEqual:

function areComponentsEqual(a, b) {
  const aType = a.originalType || a.type;
  const bType = b.originalType || b.type;

  return aType === bType;
}

This code will work without creating an element and getting an object behind a proxy. And it will work in production (without RHL)

We have a UI library (there are only UI components) and we compare element types often inside of it to implement some rendering logic.
For now, we need to include react-hot-loader as a dependency for our library to use areComponentsEqual or we need to creating an element of the UI-components to get it's type property to compare with some other element type.
We don't want to include react-hot-loader as a dependency.
Also, creating elements is not always available, cause some components have required props.

Would be great if RHL will add originalType prop to the element object and we'll can to compare element types without requiring RHL and without creating an element (example was posted above)

For libraries, which are not meant to change during developing, I would propose cold api. There is ongoing PR to use webpack loader to cold all node_modules by default.

Library authors should not be bothered by RHL (even if right now they are) - we should change RHL to be more reliable.

use webpack loader to cold all node_modules by default

Seems like it will affect linked modules (npm link) and the changes in linked libraries will not be displayed. Right?

Yep. Could be a problem for monorepo. Right now we have something like this

import { setConfig, cold } from 'react-hot-loader'

setConfig({
// this is for components defined on "top level" only
  onComponentRegister: (type, name, file) =>
    file.indexOf('node_modules') > 0 && cold(type),

// styled is created inside a function, we dont know the file name (probable doable using stack.js)
  onComponentCreate: (type, name) => name.indexOf('styled') > 0 && cold(type),
})

So the real "consumer" could have all the power upon freeze behavior, but not library itself.

@theKashey Any workaround suggestions for monorepos?

@pitops - hot-loader/react-dom(or webpack plugin) to solve type comparison for SFCs only.

v5 would solve this issue next month.

@theKashey thanks I will probably wait for v5 then

Hi,
Same issue for me, hope that v5 release will be soon.

Same issue as well.

That's implementation/design/language issue. For now for comparison you may use:

  • React.createElement(Type).type === React.createElement(Another).type
  • or <Type />.type === <Another/>.type, but it might complain about missing props
  • count on some static method -> Type.secret === SomeSymbol
  • use cold api to disable RHL proxies for some classes (and lose reload ability). That might help if you dont update the problematic components.
  • use Functional Components
  • wait will v5, which will remove proxies from classes, as v4.6 removed them from Functional ones.

The @JorgenEvens solution worked pretty well for me, although I did just a minor improvement:

const ImportedComponent = (props) => { ... }

const element = <ImportedComponent />
console.log(element.type === ImportedComponent)

The type of the instantiated component is basically a reference to the component itself, so the above approach works fine.

It would print "false". That's the problem.

v4.10.0(beta) with a patch to react-dom landed by webpack plugin(refer to docs) would ...

FIX THIS ISSUE

Just a two years later. Give it a try.

I'm still seeing this with the latest code...

currentSubmit.type === ConfirmationDialog always resolves to false
"react-hot-loader": "4.12.14"
"react-dom": "16.9.0",

@RobertGary1 - works only with hot-loader/react-dom integration. Not working without.