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

Suggestion: transforming jsx to jsx-runtime without createElement fallback #20031

Closed
morlay opened this issue Oct 15, 2020 · 46 comments
Closed
Labels
Resolution: Stale Automatically closed due to inactivity Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@morlay
Copy link

morlay commented Oct 15, 2020

Current code transform rule is not friendly for custom jsx-runtime or react-like lib.

// have to re export the `createElement`
import { createElement } from "@emotion/core"

import { jsx, jsxs, Fragment } from "@emotion/core/jsx-runtime"

Could we follow rules like below, could got same behavior.

  • <span key={key} {...obj} /> => jsx("span", obj, key)
  • <span {...obj} key={key} /> => jsx("span", {...obj, key}, key)

babel/babel#12177
microsoft/TypeScript#39199

@morlay morlay added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Oct 15, 2020
@gaearon
Copy link
Collaborator

gaearon commented Oct 15, 2020

I struggle to understand the issue — could you please write a more detailed description? I understand that the transform (intentionally) falls back to createElement but I don’t have a good sense of why this creates a problem for Emotion.

@morlay
Copy link
Author

morlay commented Oct 15, 2020

When using babel-transform-react-jsx with runtime: automatic,
and set @jsxImportSource to switch custom jsx-runtime

But if the fallback exists. codes transformed like below.

// @jsxImportSource @emotion/core

<span {...obj} key={key} />

// convert to

import { createElement } from "@emotion/core"

createElement("span", { ...obj, key: key })

and the createElement need to export by @emotion/core too (is this necessary?).
For support key after object spread (if not do this, transformed codes will got createElement not found).

now @emotion/core have as createElement-like api named jsx,
but @jsx jsx and @jsxImportSource could be set at same time.
but @jsx jsx and @jsxImportSource could not be set at same time.

I think add a custom jsx-runtime should only foucs @emotion/core/jsx-runtime with exports jsx,jsxs, Fragment

@gaearon What do you think?

@Andarist
Copy link
Contributor

Andarist commented Oct 15, 2020

but @jsx jsx and @jsxImportSource could be set at same time.

can they? arent they mutually exclusive?

I think add a custom jsx-runtime should only foucs @emotion/core/jsx-runtime with exports jsx,jsxs, Fragment

That would be IMHO preferable as well, but I guess there are good reasons why this is required right now and we'd just have to add export { jsx as createElement } from './jsx' to those new entry points, so it ain't bad. Although probably not having to worry about createElement would allow us to cut a few bundlesize bytes.

Was this fallback in the original Babel's PR? I must have missed it there as I was following that PR rather closely. Couldn't spread operator or an Object.assign call be inserted instead of falling back to createElement?

EDIT:// Oh, I have just now realized that createElement is supposed to be exported from the jsxImportSource itself and not from those new jsx-related entrypoints. I find this quite weird as well and if it's required to provide a separate @jsx pragma for this case like in this TS playground (it uses React.createElement even though @jsxImportSource is provided) then this is quite a big usability issue from Emotion's PoV.

@gaearon
Copy link
Collaborator

gaearon commented Oct 15, 2020

I'm struggling to parse the discussion on this issue so I would really appreciate if you could rephrase this imagining that I know very little about the JSX transform. All of the comments are written in a very terse way so far.

Here's what I understand so far:

  • <div key="x" {...spread} /> intentionally falls back to createElement because we can't know which key to use
  • Emotion should be able to have a createElement — but doesn't want to after the JSX changes?
    • Or is it that specifying both is a problem?

The thing I struggle with is it's not obvious to me what exactly is the "usability issue" here. It's like something implied but I don't understand what it is.

@gaearon
Copy link
Collaborator

gaearon commented Oct 15, 2020

Maybe it would help to reframe in the sense of: (1) here's the source code, (2) here's the Babel config we tell Emotion users to apply, (3) here's how they have to write code because of this issue, (4) here's how we'd like them to write code instead. Or (5) here's what we have to expose to work around this problem, (6) here's what we would prefer to expose instead. Or (7) here's what configuration they have to specify to work around this, (8) here's the configuration we want them to use instead.

@Andarist
Copy link
Contributor

Andarist commented Oct 15, 2020

Sorry for being overly terse and losing the required context. I'll try to amend it now as well as I can.

As you may know, Emotion has chosen to support the so-called css prop, and to make it work in a variety of use cases we have chosen to support it using a custom JSX pragma. We got a fair share of critique from the community for that as people like to bikeshed over this sort of thing and for some people having to use a magic comment (pragma) was too odd. We stand by the technical choices we've made and pros that it gives us - supporting 0config tools like CRA is important for us. However, after that critique from the community, we've caved a little bit and we've provided a way to insert that pragma on behalf of our consumers automatically in a form of a Babel plugin.

So, currently, there are 2 ways of how one can use css prop API:

  1. writing pragma (/** @jsx jsx */ + import { jsx } from '@emotion/core')
  2. using @emotion/babel-preset-css-prop

The jsx function that we provide is just a custom version of the createElement that either calls React.createElement if no css prop is given to it or it resolves it and then calls React.createElement using the rest of the props (the second part is a little bit simplified, but that's the gist of what it's doing).

If we consider only the second way of adding support for the css prop then nothing really changes - it's already "automatic" from our side, so it's just a matter of adding support for those new entry points etc and it should just work. Note: this has never worked with just the TS compiler which is a real bummer as it doesn't allow people to set jsx pragma globally. I've hoped that this would change with the introduction of the new runtime but I got no comment back from the TS team when I've asked about this (more than once I believe).

The problem is with the first way - writing pragma manually. I was sure that using /** @jsxImportSource @emotion/react */ would be enough to make it work. I can already hear people complaining about this new pragma (way longer than the previous one) but it is what it is - we need to support 0-config tools and TS compiler.

Let's take a look at Babel:

input
// babelrc.js
module.exports = {
  presets: ["@babel/preset-react"]
}
/** @jsxImportSource @emotion/react */

<div {...props} />;

<div {...props} key="key"/>;
output
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }

/** @jsxImportSource @emotion/react */

/*#__PURE__*/
React.createElement("div", props);

/*#__PURE__*/
React.createElement("div", _extends({}, props, {
  key: "key"
}));

Result? It doesn't work - using @jsxImportSource is not enough, one has to provide a { runtime: "automatic" } to the React's preset to make it work. It's one more thing that has to be explained, documented etc. Arguably it's not that big of a deal as 0-config tools will probably already have this option provided automatically. Some people might get lost though when configuring stuff on their own and wanting to use the pragma approach, or just because they have skipped the section on how to add Emotion's Babel plugin, or for any other reason. It feels like a small detail but I don't quite see why a sole fact of using @jsxImportSource doesn't switch the transformer to the automatic one.

Even if it works - it still requires createElement from the root of the importSource. I understand why React has to provide this (compat reasons) but the same scale and constraints do not apply to the rest ecosystem, parts of it (like Emotion) could "move" faster away from using createElement and thus saving on the implementation complexity and bundlesize of the library. Supporting this doesn't add that much to both of those - but if we could skip that entirely, why wouldn't we like to do it? It also imposes the same on other alternatives runtimes/libraries wanting to integrate with the new transform - it makes the integration more difficult, it's just one more thing to be aware of. If one sees the automatic runtime and jsx helpers the intuition (in most cases) will tell them that it's enough to make things work, but there is this special case with createElement that I feel is quite easy to miss.

Let's take a look at TS now (note - I'm using TS online playground with nightly build that is available there):

input
/** @jsxImportSource @emotion/react */

<div {...props} />;

<div {...props} key="key"/>;
output
"use strict";
/** @jsxImportSource @emotion/react */
_jsx("div", Object.assign({}, props), void 0);
React.createElement("div", Object.assign({}, props, { key: "key" }));

This has not worked as expected at all. The createElement exception has been inserted as React.createElement. To make it work as expected one would have to use both pragmas and an explicit import for the @jsx right now:

/** @jsxImportSource @emotion/react */
/** @jsx jsx */
import { jsx } from '@emotion/react'

I can only assume now that this a bug in TS nightlies but it is IMHO an indication of how easy is to miss this case and implement it incorrectly. If this is about to stay (please no) then this is a huge DX downgrade for the TS users.

@gaearon
Copy link
Collaborator

gaearon commented Oct 15, 2020

The jsx function that we provide is just a custom version of the createElement that either calls React.createElement if no css prop is given to it or it resolves it and then calls React.createElement using the rest of the props (the second part is a little bit simplified, but that's the gist of what it's doing).

Hmm, are you saying that Emotion provides jsx but it has no relation to React's concept of jsx because Emotion wanted a shorter synonym for createElement?

@gaearon
Copy link
Collaborator

gaearon commented Oct 15, 2020

Result? It doesn't work - using @jsxImportSource is not enough, one has to provide a { runtime: "automatic" } to the React's preset to make it work.

I think it would work in Babel 8 since that the new transform will become the default one?

It feels like a small detail but I don't quite see why a sole fact of using @jsxImportSource doesn't switch the transformer to the automatic one.

Yeah, I'm not sure. Maybe because they're currently implemented as two completely separate transforms. Maybe @lunaruan or @sebmarkbage knows.

Supporting this doesn't add that much to both of those - but if we could skip that entirely, why wouldn't we like to do it?

I think the createElement case is part of the reason, as we need to keep supporting <div {...stuff} key="x" /> but the new JSX signature needs key as a separate argument. How do you propose to handle that instead? It's important to preserve the current spread semantics here, otherwise it would be a huge breaking change.

In the longer run we want to make this pattern a compile error. But that would require some transitional period. If you have a custom Babel plugin you could make that a compile error today, but this doesn't help the zero-config case.

This has not worked as expected at all. The createElement exception has been inserted as React.createElement

You didn't show a successful example, but I think you're saying that Babel would instead produce code with two auto-imports rather than one. Is this correct? Does Babel have the "severe DX downgrade" aside from a longer pragma?

I can only assume now that this a bug in TS nightlies but it is IMHO an indication of how easy is to miss this case and implement it incorrectly. If this is about to stay (please no)

I don't understand the reasoning here. It does sound like a bug in TS, because it doesn't match the behavior in Babel and in our tests. If it's a bug in TS, it's only "about to stay" if you don't report it to TS and they don't fix it. So it seems like the next action item is to report it.

I don't see why "easy to miss" is an important consideration here — it's only "easy to miss" once per JSX implementation. Today, we have two mainstream ones (Babel and TS). I think it's expected that transform implementations are nuanced. ES spec nuances are also easy to miss, but this doesn't preclude Babel from getting them right.

Assuming the TS gets reported and fixed, is the "DX downgrade" still severe?

@morlay
Copy link
Author

morlay commented Oct 15, 2020

@gaearon

My concern is why we need do the fallback.

if we just want to pass key prop, i think

<span {...obj} key={key} /> => jsx("span", {...obj, key}, key)

could got same result with jsx-runtime

Is there other reasons to fallack createElement (only for key after props spread, key before props spread already using jsx-runtime)?

  • <div key="x" {...spread} /> intentionally falls back to createElement because we can't know which key to use

<div key="x" {...spread} /> not fallback.

babel-config: { plugins: ["@babel/plugin-transform-react-jsx", { "runtime": "automatic" }]}

<span {...props} key={"key-after"} />
<span key={"key-before"} {...props} />

// convert to 

import { jsx as _jsx } from "react/jsx-runtime";
import { createElement as _createElement } from "react";
    
 _createElement("span", { ...props,
      key: "key-after"
 })
_jsx("span", { ...props }, "key-before")

@morlay
Copy link
Author

morlay commented Oct 16, 2020

@Andarist

There is a point i confused - "only key after props spread do the fallback"

/** @jsxImportSource @emotion/react */

<div key="key "{...props} />;

<div {...props} key="key"/>;

// will convert to 

import { createElement } from "@emotion/react"
import { jsx } from "@emotion/react/jsx-runtime"

jsx("div", props, "key")
createElement("div", { ...props, key: "key" }) // only key after props spread do the fallback 

i don't know why not convert to jsx("div", props, "key") for both cases.

it could be jsx("div", { ...props, key: "key" }, "key")
if want to pass key into component as babel/babel#12177 (comment) mentioned

@morlay
Copy link
Author

morlay commented Oct 16, 2020

I think the createElement case is part of the reason, as we need to keep supporting <div {...stuff} key="x" /> but the new JSX signature needs key as a separate argument. How do you propose to handle that instead? It's important to preserve the current spread semantics here, otherwise it would be a huge breaking change.

@gaearon

i still think <div {...stuff} key="x" /> transforming tojsx("div", stuff, "x") will not break anything (props.key is always undefined in Component ).

However. current tools transforming <div {...stuffWithKeyProp} /> tojsx("div", { ...stuffWithKeyProp }),
Which will cause the issue you mentioned. When user upgrade to jsx-runtime, key missing error will be throw.

@morlay
Copy link
Author

morlay commented Oct 16, 2020

By infomation from https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#deprecate-spreading-key-from-objects

i think the transforming rules could be unified:

  • <div {...stuffWithKeyProp} /> => jsx("div", stuffWithKeyProp)
    • if key needed, warning key missing
  • <div key="key-before" {...stuff} /> => jsx("div", stuff, "key-before")
  • <div {...stuff} key="key-after" /> => jsx("div", stuff, "key-after")

the key is the special prop as fact rule for a long time.
still let it continue special until it renamed to @key

even support @key too.
if @key not exists, but key exists, show the deprecating warning and using key as fallback of @key, and do the merging.

  • <div @key="key-before" {...stuff} /> => jsx("div", { ...stuff }, "key-before")
    • <div key="key-before" {...stuff} /> => jsx("div", { key: "key-before", ...stuff }, "key-before")
  • <div {...stuff} @key="key-after" /> => jsx("div", { ..stuff }, "key-after")
    • <div {...stuff} key="key-after" /> => jsx("div", { ...stuff, key: "key-after" }, "key-after")

@ljharb
Copy link
Contributor

ljharb commented Oct 16, 2020

Considering that the last key prop (or any prop) should win, it’s very confusing that #20031 (comment) seems to be doing the exact opposite of what would be needed.

@morlay
Copy link
Author

morlay commented Oct 16, 2020

@ljharb you mean confusing for the current transforming results or my suggestion?

@ljharb
Copy link
Contributor

ljharb commented Oct 16, 2020

The results. What I would expect is that <div {...stuff} key="key-after" /> would be the one that can be handled with the new transform, and that <div key="key-before" {...stuff} /> could not be, because stuff.key would overwrite key-before in the latter example.

@Andarist
Copy link
Contributor

Hmm, are you saying that Emotion provides jsx but it has no relation to React's concept of jsx because Emotion wanted a shorter synonym for createElement?

Not sure if I understand the question correctly but Emotion's jsx is just a wrapper around React.createElement. The name (jsx) has probably been choosen as it's shorter and to make it different from the React's one - which was a gain in the times when people had to write those pragmas mostly manually.

I think it would work in Babel 8 since that the new transform will become the default one?

Yes - that would probably work out of the box in Babel 8 but there will be Babel 7 consumers for a considerable amount of time in the future so I still believe it's a problematic from the DX point-of-view (as mentioned - it's not a super big deal, but it could not exist at all, just like in the TS compiler).

Yeah, I'm not sure. Maybe because they're currently implemented as two completely separate transforms. Maybe @lunaruan or @sebmarkbage knows.

Their comment about this would be highly appreciated - those transforms are mostly internal in Babel right now, so this could always be refactored rather easily to merge them into 1 or something. I believe it would make sense as it would cover more cases and in the end of the day it would be more intuitive.

I think the createElement case is part of the reason, as we need to keep supporting <div {...stuff} key="x" /> but the new JSX signature needs key as a separate argument. How do you propose to handle that instead? It's important to preserve the current spread semantics here, otherwise it would be a huge breaking change.

I'm slightly confused by this - in the similar way @ljharb is. Why this is a problem if we know what key is going to "win" and not the opposite situation where key is defined before the spread? Also - why <div {...props} /> doesn't have to call createElement nor it provides an extra argument to React.jsx for the key? After all, props could include a key prop and you want to preserve the current semantics without breaking people right now, right? I bet answers to those could be found in the original RFC or in the Babel's PR but they are quite lenghty. I know it's not super fair - but I would appreciate pointers to the required background behind this or a short explanation here. I'm commenting on this right now between taking care of 2 kids & packing bags for a trip. I totally understand that you won't be able to provide those due to your own time constraints though so if you don't do that I will try to dig those things up in the following days.

You didn't show a successful example, but I think you're saying that Babel would instead produce code with two auto-imports rather than one. Is this correct? Does Babel have the "severe DX downgrade" aside from a longer pragma?

This particular input/output pair was from the TS compiler. The first one was from Babel and it has worked OK (after adding { runtime: "automatic" } option which is a little annoyance from my PoV as mentioned above) - it has inserted 2 auto-imports and used configured importSource (through @jsxImportSource pragma) for the createElement import. This is OK from consumers' PoV - they don't really care about what is inserted as it happens automatically. From the library maintainer's PoV it is weird to be forced to provide an extra export from the root entry (usually) of the package if this whole thing has introduced dedicated entry points for the JSX-related factories. But to sum this up - I haven't recognized major DX downgrades in Babel alone.

I don't understand the reasoning here. It does sound like a bug in TS, because it doesn't match the behavior in Babel and in our tests. If it's a bug in TS, it's only "about to stay" if you don't report it to TS and they don't fix it. So it seems like the next action item is to report it.

Yes, I was just wanting to establish in the latest comment that I should report a bug to the TS team about this. Would be cool to settle this discussion as a whole before doing so, so I could report everything at once if we find out that there is anything else worth reporting.

I don't see why "easy to miss" is an important consideration here — it's only "easy to miss" once per JSX implementation. Today, we have two mainstream ones (Babel and TS). I think it's expected that transform implementations are nuanced. ES spec nuances are also easy to miss, but this doesn't preclude Babel from getting them right.

Sure, there are always be nuances but I feel that if we could avoid them then the overall result would be better and it feels to me that this is just a case like this. This nuance could not exist - I'm purely referring here to the fact that createElement is not part of the dedicated entry points in the new transform.

Assuming the TS gets reported and fixed, is the "DX downgrade" still severe?

No, they are not super severe. I've jumped into this issue first after seeing TS output - assuming they got it "right", especially that I've just learned about createElement being used at all in the new JSX transform. I couldn't test this in Babel at the time because this can't be configured in the playground - again, kinda because @jsxImportSource is not enough to trigger the new transform to kick in. Later I've setup local test and the situation is much better than I have thought at first but there are still those minor annoyances that I've mentioned in this comment and the earlier one. I totally understand that you won't classify them as severe (I don't either) or even recognize them as annoyances at all (although I believe that they are).

@gaearon
Copy link
Collaborator

gaearon commented Oct 16, 2020

Sorry for confusing replies from my side. I’m just picking up this work so I don’t have all the context and am also confused by some things. I personally wish this stuff was brought up a little bit earlier than a stable release including the transforms. We published the first RC in August (although to be fair we didn’t emphasise the runtime) and the JSX blog post almost a month ago. But I appreciate that it’s being raised. I just need a bit more time and context to digest this thread and respond to the comments.

@gaearon
Copy link
Collaborator

gaearon commented Oct 16, 2020

Ok so I've done some digging and I think I have a slightly better idea of what's going on. Scratch my previous answers.

The End Goal

Like the RFC says, we want to get here:

function jsx(type, props, key) {
 return {
   $$typeof: ReactElementSymbol,
   type,
   key,
   props,
 };
}

The rest is tactics.

Key and Spread

We have a few cases we can't break right away. That's our first constraint.

let obj = { key: "bar" }

// 1. Key Before Spread
<div key="foo" {...obj} />.key // "bar"

// 2. Key After Spread
<div {...obj} key="foo" />.key // "foo"

In the longer term, we either want to make Key Before Spread a compile error (due to ambiguity) or, more likely, just make both of them use "foo" as a key. Potentially changing the syntax to @key= in distant future to highlight it's not in the same "namespace" and thus doesn't get affected by spreading.

Key Before Spread Deprecation

Since key before spread which has a key in it is a problematic pattern that will change the behavior, we'll need to start warning about it at some point. Now here's the fun bit. The traditional createElement() transform can't produce code warning about it.

// Classic transform

<div key="foo" {...obj} />.key // "bar"

// ->

React.createElement(
  "div",
  _extends({ key: "foo" }, obj)
).key; // "bar"

In this case, createElement just sees the already merged { key: "bar" } as its second argument, so it can't know whether we have, in fact, had a spread with an overwritten key there. We can't rely on createElement to show that deprecation warning because it doesn't get enough information to detect the bad pattern.

It would have to be done by a different compile target. This is why rolling out the new JSX transform widely is so important. It enables the new runtime warning.

Should jsx prefer config.key or maybeKey?

jsx(type, config, maybeKey)

In the end state, we just use maybeKey. This is clear enough. But what do we do in the meantime? We have two options:

  1. Config Wins: We use config.key if it's not undefined, otherwise we use maybeKey.
  2. Key Wins: We use maybeKey if it's not undefined, otherwise we use config.key.

There's a few things we can note about these options:

  • If Key Wins, we can't easily distinguish between <div {...obj} key={undefined} /> (override key with undefined) and <div {...obj} />. This is because maybeKey would be undefined in both cases. So how do we distinguish? We could hack something like look at arguments.length but that's not ideal for a fast implementation.
  • If Key Wins, we have to forbid <div key="foo" {...obj} /> and make it fall back to createElement. Because it would have gotten an incorrect key ("foo"). But the problem is that, as we said in the section above, createElement cannot add a warning for this case because the merging would happen before the call. From createElement perspective, it would just be a regular call with { key: "bar" }. So then we wouldn't be able to deprecate this pattern, which is the whole point.

For these reasons, we went with Config Wins until breaking changes. Which means that we use config.key, and only fall back to maybeKey if that doesn't exist. This lets us use jsx() for <div key="foo" {...obj} /> (config wins!), which can later add a deprecation warning specifically for this pattern (because it knows config.key and maybeKey clash).

However, if Config Wins, we can't use jsx() for <div {...obj} key="foo" /> today. This is because it would incorrectly get the "foo" key. This is why, counter-intuitively, Key After Spread doesn't use jsx() today and falls back to createElement.

Summary

Let's say:

let obj = { key: "bar" }

Here's what we have.

Today

  • <div {...obj} key="foo" />: The key is "foo". Always uses createElement.
  • <div key="foo" {...obj} />: The key is "bar". Uses jsx() with the new transform.

Stage 1 (Future Warning in React)

  • <div {...obj} key="foo" />: The key is "foo". Still uses createElement.
  • <div key="foo" {...obj} />: The key is "bar". Uses jsx() with the new transform, which warns.

Stage 2 (Future Breaking Change in React)

  • <div {...obj} key="foo" />: The key is "foo". Uses jsx() with the new transform
  • <div key="foo" {...obj} />: The key is "foo". Uses jsx() with the new transform.

@ljharb
Copy link
Contributor

ljharb commented Oct 16, 2020

@gaearon thanks, that's a very thorough analysis. Should eslint-plugin-react have a rule to help prevent combined usage of key and spread? Should it warn on both combinations or just one of them? What would be most helpful here?

@gaearon
Copy link
Collaborator

gaearon commented Oct 16, 2020

I think we'll want to warn for spread after key because that's the one that would eventually change the behavior.

@gaearon
Copy link
Collaborator

gaearon commented Oct 16, 2020

The tricky case is when you have <div {...obj} /> and you don't know whether there's a key in there. Because static analysis can't catch that. That's the most important one we'll need to warn about at runtime.

@morlay
Copy link
Author

morlay commented Oct 17, 2020

The tricky case is when you have <div {...obj} /> and you don't know whether there's a key in there. Because static analysis can't catch that. That's the most important one we'll need to warn about at runtime.

react/jsx-key may help to warn in where key required.

runtime warning may be confused. if we could pass key into Component with jsx-runtime.

@morlay
Copy link
Author

morlay commented Oct 17, 2020

@gaearon Thanks for your explanation. the final stage is same as i expect.

I have switch to using jsx-runtime successfully when 16.4 landed.
Just confused by the createElement fallback.
We could use other tool to avoid key after spread.

After all tool chain ready.
hope the createElement fallback may be documented like https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html,
it is important for react-like or jsx wrapper packages.
At least, maintainers need to be notified the fallback rule for pushing forward to jsx-runtime smoothly

For developer, may need npx react-codemod re-sort-react-key.

@Andarist
Copy link
Contributor

I personally wish this stuff was brought up a little bit earlier than a stable release including the transforms. We published the first RC in August (although to be fair we didn’t emphasise the runtime) and the JSX blog post almost a month ago. But I appreciate that it’s being raised.

Sure thing - that would be ideal. It's not that I haven't been paying attention to this at all, it's just that I probably have assumed some stuff and had to handle other stuff, my OSS backlog is only growing 😭 I was actually looking into both the original RFC (which doesn't mention createElement fallback at all - probably because it wasn't yet decided how to handle this key problem at this point in time) and I've also participated in the discussion below the original Babel PR. I've raised some concerns there but I've totally missed the createElement fallback stuff.

My concerns about pragma stuff were heard by Nicolo and I've responded back that what he has proposed was addressing my concerns but it seems that at the end of the day it was not implemented as described in Nicolo's comment or we've misunderstood each other. I'm talking about this comment that mentions pragmas as only requirement for chosing classic/automatic runtime (when pragmas are actually used, ofc). From that comment I did not conclude that for the new pragma to actually work an additional option passed to the preset itself would still be required (as I believe that was not Nicolo's intention there). I'm not trying to blame anyone - don't get me wrong. There were a lot of comments below this PR, I was just an observer/commenter and it was easy to miss stuff and not address every little detail.


Thank you for the thorough explanation of the createElement fallback stuff! This is very helpful in understanding why this works how it does.

I understand now why this is needed for React. I know that you care deeply about compatibility, you have a huge ecosystem to support etc. The recent introduction of the new factories to the 3 last major versions of React is just a sign of this and this is great.

That being said - the JSX, even though it has originated in React, became more of its own thing that is decoupled from React. It seems that the whole point of new transforms is to allow slightly different semantics and optimizations opportunities - those are interesting from the PoV of all parties using JSX, but not all of them are having the same constraints as React in terms of the ecosystem and compatibility. IMHO this should act as a fresh start by default and only provide means for the React (and parties interested in the same) to allow for the smoother migration process. Some parties could just not care about this migration and jump right into the final state of things - instead they are forced to go along the React's migration process. For example in the case of Emotion this might need more syncing points in the future with React's release schedule than we'd like. Maybe we'd like to follow your path closely - I don't know. The point is that this choice has been taken away from us to some degree and I think that we should have this choice. Even more so runtimes not tied to React at all should have that choice. Just my 2 cents about this.


Assuming the TS gets reported and fixed, is the "DX downgrade" still severe?

Circling back a little bit to this - after internalizing more facts about this. If we just talk about the thing in abstract - no, the DX downgrade is not severe (apart from minor annoyances that we've mentioned here). However, if we take into consideration how this works in Babel now then - yes, we could call this quite severe.

Why? Solely because of the fact mentioned in this very comment of mine: pragmas alone are not reconfiguring the transform accordingly. 0-config tools have to use some kind of detection to configure the "runtime" option, for example CRA is (right now) configuring this here. This has a following effects:

Emotion+CRA users won't be able to upgrade CRA seamlessly because they are using @jsx pragma in their code and this, in combination with "runtime": "automatic", throws an error:

SyntaxError: /test__/index.js: pragma and pragmaFrag cannot be set when runtime is automatic.
> 1 | /** @jsx jsx */
    | ^
  2 | 
  3 | <div {...props} />;

Which is also somewhat weird because the opposite situation (@jsxImportSource with "runtime": "classic") does not throw an error. It just doesn't do anything.

I believe this is somewhat a deal-breaker kind of severity if the overall intention is to provide the most seamless experience possible. However, it can be fixed rather easily - by allowing pragmas to reconfigure the used runtime (which would match the current default behavior of the TS compiler).

@gaearon
Copy link
Collaborator

gaearon commented Oct 17, 2020

No problem at all, I appreciate the feedback! I maybe have been unclear myself earlier — what I’m saying is I wasn’t aware of this nuance either, and even people working on it got confused by it a few times while explaining it to me. It’s inherently confusing because of the migration path constraints. I agree we should’ve stressed this case in more detail to the TypeScript team. The only thing I disagreed with in your statement was the implication that because something is not obvious or confusing, it demonstrates a flaw in the approach. I think it’s important to separate lapses in communication (which is what happened here) from lapses in the long term plan and the upgrade path itself. Criticism of both is welcome but we need to separate them because otherwise it’s hard to understand which of the sentiment people are expressing. I really appreciate the conversation!

@gaearon
Copy link
Collaborator

gaearon commented Oct 17, 2020

as the "fix" would require reexporting createElement from those new JSX entries and adjusting the Babel transform but you probably don't want to break early adopters and mismatch what's exported from those between React versions. Unless you are open to that.

FWIW we don’t want to re-export because we want to keep the new runtime minimal. Not because of the transform that has shipped per se. It doesn’t quite make sense to us to re-export createElement from React when the point is to start moving away from it as a compile target.

I don’t think I quite agree with your argument that “newer” environments like Emotion can jump faster to modern runtime and abandon the classic runtime altogether. The reason I don’t agree with it is because when a person adopts Emotion, they don’t intend to adopt different spread semantics. They wouldn’t even be aware of such a difference. It would be confusing if I converted a project to Emotion, and suddenly my keys stopped working, leading to (e.g. in case of a messenger app) input state being shared between different people’s conversations. I think it’s important that aside from the css prop, JSX semantics work the same way in Emotion and React, which means that Emotion jsx() runtime needs to proxy to React’s jsx(), and Emotion createElement() runtime needs to proxy to React’s createElement().

The problem with the bruteforce approach of Emotion just having a modern JSX entry point is that it won’t be synced with React’s gradual migration path, deprecation warnings, etc. This will likely become a problem in the following years if they don’t match 1:1. I think it would be preferable that Emotion doesn’t implement the JSX runtime at all (and tells the users to keep using the classic transform) than if it implements it with different semantics and causes difficulties with the rest of the migration path.

@Andarist
Copy link
Contributor

The only thing I disagreed with in your statement was the implication that because something is not obvious or confusing, it demonstrates a flaw in the approach. I think it’s important to separate lapses in communication (which is what happened here) from lapses in the long term plan and the upgrade path itself.

I'm not saying that the overall approach was fundamentally flawed, just rather than a different approach would not cause such confusion and that there would not be a need to communicate this.

FWIW we don’t want to re-export because we want to keep the new runtime minimal. Not because of the transform that has shipped per se. It doesn’t quite make sense to us to re-export createElement from React when the point is to start moving away from it as a compile target.

I don't quite see how really the current situation is different from the proposed one. You will have a point in the future when you are going to deprecate/remove the createElement anyway and I don't see how the location of that export changes the overall scheme of things. Either way - this is already bikeshedding-stuff now, so let's drop this.

I don’t think I quite agree with your argument that “newer” environments like Emotion can jump faster to modern runtime and abandon the classic runtime altogether. The reason I don’t agree with it is because when a person adopts Emotion, they don’t intend to adopt different spread semantics.

Yes, I agree that we would probably choose to match your release timeline - having a choice would be appreciated though. In the same way - it would be really cool for end consumers to opt-in to the final state of those semantics. I personally would prefer to just migrate now in a one go and stick to the new semantics from now on rather than migrate a little bit now and later have to circle back to this again. I totally understand this might cause a lot of churn in the community and a migration path forward is really important but it's not as important to all people.

Dropping the argument for Emotion potentially using new semantics - there is still a high-level problem of non-React runtimes being bound to your migration path and I believe that this problem is "real". It's, of course, not super terrible, but it exists - with the change of the transform the complete new semantics should be established (from the overall discussion I got a feeling that it's still undecided how to handle key in the end - only that it's most likely be "renamed" to @key, but this has not been fully decided yet) and other runtimes, old ones or new ones, should be able to use them from now on. Especially for new runtimes it doesn't make sense to be forced to implement the legacy exports now.

@eyelidlessness
Copy link

Apologies for wandering into a 10 month stale discussion with questions about motivations for long-stable functionality.

My own motivation, for context

I’m working on an ESM loader for Node to automatically transpile with ESBuild. This is something that seems like it would be trivial, and in fact there are a couple implementations already out there. Both are fairly incomplete in a variety of ways, enough so that I’m more comfortable building my own than trying to contribute to existing implementations.

In the course of my own work on this, I’ve been watching two open issues hoping they’d gain some traction in tandem with my own efforts, but it seems pretty unlikely they will given they’re similarly stale.

So my next (naive) hope was to get enough context that I could offer to contribute a solution. I already knew the jsx signature differs, from my own naive WIP implementation, but didn’t have much context for why. The comments here and reading the original RFC have given me a lot of context for what: the key prop is treated specially, in a way it hasn’t been previously, and is explicitly described as such in the RFC. But it’s given me very little why: the underlying motivation for treating key or an eventual @key prop differently from others.

I’m sure there’s plenty to find and read on the subject. Unlike an earlier comment I’m explicitly asking if someone will help me shortcut to understanding. I also know that’s unfair to ask, however…

This Node loader project isn’t the thing I’m interested in working on, it’s just a yak I’ve been shaving to try to get around it, and it keeps growing weight and fur. My goal is to have a reliable and consistent JSX and TS runtime in Node, and most importantly have it perform well in tests (ruling out ts-node or anything depending on tsc, also ruling out Babel; both are great but quite slow for TDD)… so I can build other stuff, and so other people can too.

So… key

Asking plaintively: why is key special in JSX? What begat this complexity? Why does everything have to be difficult?

Asking more imperatively…

JSX, when introduced, was a jarring concept. Embedding HTML (or XML) in JS was extraordinarily taboo. But the actual pitch was great: JSX is just syntactic sugar for plain JS expressions. It’s so close to hyperscript that the biggest React alternative using JSX named their createElement as an homage to that, and h appears in nearly every JSX runtime to this day.

It’s cognitively easy to adopt and adjust to JSX by mentally translating <Foo bar={ quux } /> to Foo({ bar: quux }). Lots of JSX implementations have specific optimization techniques, rules around what kinds of expressions are allowed or can be optimized. (See proliferation of control flow components in non-React implementations.)

But it’s troubling to see a syntax-level intervention that specially handles references by name. It’s already troubling with ref even though that’s widely known and accounted for. It’s much more troubling as a semantic breaking change for users who can no longer mentally model <Foo bar={ quux } key={ computeKey(x) } /> as Foo({ bar: quux, key: computeKey(x) }).

So why is key special enough to warrant this complexity?

More to the point (for my efforts to support the change in tandem with ESBuild), why can’t this complexity be implemented in the transform and by libraries, so there isn’t so much ambiguity? If the new transform is opt-in, whatever semantic differences could be part of the spec (i.e. jsx-runtime provides checks on spread).

More to the point (for people understanding JSX), why does a new special case need to exist and undermine the understanding that props is a dictionary of argument options?

@markerikson
Copy link
Contributor

@eyelidlessness semi-serious question: are you actually familiar with what key actually does in React?

React does not pass key through to a component as an actual prop. In fact, I've seen users assume they can render <MyComponent key="abcd"> and read props.key inside, and it's always undefined.

Because of this, there's both a conceptual and a runtime difference between a field named key in what will eventually become props, and all the rest of the actual prop values.

(Apologies if you are familiar with this already - you clearly are working on some complicated stuff here, but from the context of the question it does sound like you're asking why key even exists in the first place.)

@stale
Copy link

stale bot commented Jan 9, 2022

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Jan 9, 2022
@iMrDJAi
Copy link

iMrDJAi commented Aug 6, 2022

bump

@stale stale bot removed the Resolution: Stale Automatically closed due to inactivity label Aug 6, 2022
@iMrDJAi
Copy link

iMrDJAi commented Aug 6, 2022

The createElement fallback breaks compatibility and consensus. The "key" prop is React specific and should not be treated specially in the context of custom JSX runtimes. JSX meant to be a way to embed inline XML into JavaScript code. The current spec is broken and changes need to be done. It needs to consider that and completely separate React from JSX.

@ljharb
Copy link
Contributor

ljharb commented Aug 6, 2022

I don’t think appeals to “meant to” are going to help you here; jsx is “meant to” be used for React, it’s just become used more broadly.

@iMrDJAi
Copy link

iMrDJAi commented Aug 6, 2022

@ljharb Yes, React was the reason of the popularity of JSX, however JSX was a thing even before its adoption by React (For example https://en.wikipedia.org/wiki/ECMAScript_for_XML).
Since it become used more outside React, it's a good idea to make it more universal and less React specific, and most importantly, easier for other developers to customize.

@ljharb
Copy link
Contributor

ljharb commented Aug 6, 2022

@iMrDJAi the concept of "xml in JS" certainly was, but JSX, specifically, was created by the react team for use in react.

@iMrDJAi
Copy link

iMrDJAi commented Aug 10, 2022

@ljharb Regardless of the origins of JSX, and the createElement fallback, I have managed to create my own custom runtime that transforms JSX into Non-Compact objects that can be converted into plain XML. 😅

https://github.com/iMrDJAi/xml-jsx

@ljharb
Copy link
Contributor

ljharb commented Aug 10, 2022

@iMrDJAi congrats! I’m not saying at all that jsx should only be for react; I’m just saying an appeal to original intentions won’t help you :-)

@iMrDJAi
Copy link

iMrDJAi commented Aug 11, 2022

@ljharb Regardless of the origins of JSX, and the createElement fallback, I have managed to create my own custom runtime that transforms JSX into Non-Compact objects that can be converted into plain XML. 😅

https://github.com/iMrDJAi/xml-jsx

Update: I have added docs and published the package under a different name: https://github.com/iMrDJAi/xml-jsx-runtime.
It was really a fun experiment. :)

Copy link

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@github-actions github-actions bot added the Resolution: Stale Automatically closed due to inactivity label Apr 10, 2024
Copy link

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Resolution: Stale Automatically closed due to inactivity Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

7 participants