Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Element type comparing #304

Closed
Mistereo opened this issue May 29, 2016 · 76 comments
Closed

Element type comparing #304

Mistereo opened this issue May 29, 2016 · 76 comments
Milestone

Comments

@Mistereo
Copy link

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?

@nfcampos
Copy link
Collaborator

nfcampos commented Jun 2, 2016

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?

@Mistereo
Copy link
Author

Mistereo commented Jun 2, 2016

@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.

@nfcampos
Copy link
Collaborator

nfcampos commented Jun 3, 2016

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

@JorgenEvens
Copy link

JorgenEvens commented Jun 9, 2016

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)

@singggum3b
Copy link

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

@w3apps
Copy link

w3apps commented Jun 15, 2016

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

@harmony7
Copy link

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.

@c58
Copy link

c58 commented Aug 1, 2016

I'm using another approach

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

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

@phyllisstein
Copy link
Contributor

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 ?

@shenbin04
Copy link

@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.

@elado
Copy link

elado commented Oct 11, 2017

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

@Lohann
Copy link

Lohann commented Oct 22, 2017

+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) {
    ...
  }
})

@lgra
Copy link

lgra commented Nov 6, 2017

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)
        }
      }
    }
  }
}

@theKashey theKashey self-assigned this Nov 6, 2017
@theKashey
Copy link
Collaborator

@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 Keep InitialComponent reference in ProxyComponent  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?

@lgra
Copy link

lgra commented Nov 6, 2017

@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.

@theKashey
Copy link
Collaborator

theKashey commented Nov 7, 2017

@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.

@lgra
Copy link

lgra commented Nov 7, 2017

@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.

@theKashey
Copy link
Collaborator

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.

@gaearon
Copy link
Owner

gaearon commented Nov 8, 2017

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

@theKashey
Copy link
Collaborator

@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.

@pitops
Copy link

pitops commented Jan 14, 2019

@theKashey Any workaround suggestions for monorepos?

@theKashey
Copy link
Collaborator

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

v5 would solve this issue next month.

@pitops
Copy link

pitops commented Jan 18, 2019

@theKashey thanks I will probably wait for v5 then

@wijdench
Copy link

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

@joanned
Copy link

joanned commented Jan 22, 2019

Same issue as well.

@theKashey
Copy link
Collaborator

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.

@carloseduardosx
Copy link

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.

@theKashey
Copy link
Collaborator

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

@theKashey
Copy link
Collaborator

theKashey commented Jun 2, 2019

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.

@RobertGary1
Copy link

RobertGary1 commented Sep 23, 2019

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",

@theKashey
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests