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

Add experimental version of the babel-plugin-transform-react-jsx transform #11154

Merged
merged 10 commits into from Mar 17, 2020

Conversation

lunaruan
Copy link
Contributor

@lunaruan lunaruan commented Feb 19, 2020

Q                       A
Fixed Issues?
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature? Yes
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

As part of React's RFC to simplify element creation, we want to update the babel-plugin-transform-react-jsx transform so that it uses React.jsx instead of React.createElement and adds to ability to auto import React for JSX.

The main differences between React.jsx/React.jsxDEV and React.createElement are:
1.) key is now passed as an explicit argument rather than through props
3.) children are now passed through props rather than as an explicit argument
4.) props must always be an object
5.) __source and and __self (added through separate Babel plugins) are now passed as separate arguments into React.jsxDEV rather than through props
6.) In production, we will use jsxs(type, props, maybeKey) if there MORE THAN ONE child in the AST and jsx(type, props, maybeKey) if there is ONE OR FEWER children in the AST.
7.) We want to deprecate key spread through props, so we will use React.createElement to transform the case where props spread comes before a key (ex. <div {...props} key="Hi" />) so that we differentiate between prop spread after a key (ex. <div key={foo} {...props} />, which will override the key) and warn appropriately.
8.) In development mode we will use jsxDEV(type, props, key, isStaticChildren, source, self), where isStaticChildren is true when you should use jsxs and false if you should use jsx. This will be specified using a mode: development option. (One concern here is that someone might accidentally forget to set this. We can make this option required so that this doesn't happen)

This PR also adds two options for auto import to React:
1.) importSource: The React module to import from. Defaults to react.
2.) runtime: 'classic' | 'automatic'. In Babel 7.9 this will default to classic and in Babel 8 it will default to automatic. In classic mode, we will use createElement without auto importing (ie. the current behavior). In automatic mode, we will use jsx/jsxs and auto import. If the sourceType is module, we will use named exports (import {jsx as _jsx} from "react"; JSX compiles to _jsx etc.). If the sourceType is script, we will use require (var _react = _interopRequireWildcard(require("react"));. jSX compiles to _react.jsx etc.)

Automatic runtime adds two pragmas (jsxAutoImport and jsxImportSource) that allow users to specify autoImport and importSource in the docblock. Classic runtime will still have pragma and pragmaFrag.

In addition, for Babel 8, development mode will require a separate Babel plugin, @babel/plugin-transform-react-jsx-development. This mode will automatically add __source and __self for both runtimes so that users will not need to use separate plugins. Adding the source and self plugins will be a no-op.

@existentialism existentialism added area: jsx PR: New Feature 🚀 A type of pull request used for our changelog categories labels Feb 19, 2020
@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Feb 19, 2020

1.) importSource: The React module to import from. Defaults to react.
2.) autoImport: The type of import. Defaults to none.
...

I think that this option has too many possible values. There are only three possible cases:

  1. The jsx function is a named export of the module (considering import foo as a shorthand for import { default as foo }, thus the name is default)
  2. The jsx function is a property of a named exported object (again, default is a normal name).
  3. The jsx function is a property of a global object.

(1) and (2) can be "collapsed" together, using the object.property notation that we are already using for the pragma option.
(3) can be obtained by disabling importSource.

The require vs import case should be automatically handled by @babel/helper-module-imports, which detects the current sourceType.

In these examples I'm using the pragma option name rather than autoImport, because I think that it fits better my proposed changes. Keep in mind that some of the examples in the following table aren't valid React imports, but they might be useful for other libraries.

Input code: <div />

sourceType importSource pragma output
module false React.jsx React.jsx("div")
script false React.jsx React.jsx("div")
module "react" jsx import { jsx } from "react";
jsx("div");
script "react" jsx const _react = require("react");
(0, _react.jsx)("div");
module "react" default import { default as _react } from "react";
_react("div");
script "react" default const _react = _interopRequireDefault(require("react"));
(0, _react.default)("div");
module "react" default.jsx import { default as _react } from "react";
_react.jsx("div");
script "react" default.jsx const _react = _interopRequireDefault(require("react"));
(0, _react.default).jsx("div");
module "react" jsx.dev import { _jsx } from "react";
_jsx.dev("div");
script "react" jsx.dev const _react = require("react");
(0, _react.jsx).dev("div");

This avoids hard-coding the jsx/jsxDEV names since other libraries use other IDs (e.g. h). We can still use jsx/jsxDEV as the default values.

We would probably also need pragmaStatic/pramaMulti, pragmaDev and pragmaFrag options.
Or maybe pragma: { single: "React.jsx", multiple: "React.jsxs", dev: "React.jsxDEV", frag: "React.Fragment" }.

7.) We want to deprecate key spread through props, so we will use React.createElement to transform the case where props spread comes before a key (ex. <div {...props} key="Hi" />) so that we differentiate between prop spread after a key (ex. <div key={foo} {...props} />, which will override the key) and warn appropriately.

I'm not sure I follow. Why doesn't this work?

const props = { key: "foo" };
React.jsx("div", { ...props}, "Hi");

You'll still see that the second parameter contains a key property, because the "real" key is passed as a different parameter.

// as a sequence expression to preserve order. So:
// <div children={x++} foo={y}>{child}</div> becomes
// React.jsx('div', {foo: (x++, y), children: child});
// duplicateChildren contains the extra children prop values
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider throwing in the transform if there are both syntactic children and the children property.

If we don't want to, we don't need this custom logic anyway. This output has the same effect:

React.jsx("div", {
  children: x++,
  foo: y,
  children: child,
});

Copy link
Contributor Author

@lunaruan lunaruan Feb 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually did this at some point but it broke on a older browser because duplicate keys caused an error, so we had to do it this way instead. I'll add that to the comment though to clarify.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a period in ECMAScript's lifetime when duplicate keys wasn't allowed in strict mode. Unfortunately those browsers are still around. Babel should probably have a transform to remove duplicate keys from any object literal.

We could error but that would make it harder to upgrade. Since we did run into this, we know people write this code today. Arguably it shouldn't error for the same reasons ECMAScript doesn't error on duplicate keys.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Babel already has that transform. If people need to support a browser which throws for duplicate keys, it's handled by preset-env.

Since we are going to enable object spread by default in the jsx transform, most people will have preset-env anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the transform accounts for duplicate keys? I guess since this is for Babel 8 it's probably okay to introduce this as a breaking change, since we're also doing things like defaulting to spread anyway. @sebmarkbage

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's https://babeljs.io/docs/en/babel-plugin-transform-duplicate-keys.

You can see it in action here: https://babeljs.io/repl/#?browsers=ie%2011&build=&builtIns=false&spec=false&loose=false&code_lz=G4QwTgBCELwQ3gKAhAHgLggRgDTLZgEyIC-QA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=env%2Cenv&prettier=false&targets=&version=7.8.6&externalPlugins=%40babel%2Fplugin-transform-async-to-generator%407.8.3

It produces a sub-optimal output, but:

  • The only popular browser that needs it is IE (these browsers support them)
  • If we want to produce a more optimized output by merging the first duplicate key into the following one, it should be done in @babel/plugin-transform-duplicate-keys and not here
  • Duplicate keys are super rare anyway

);
}

if (useSpread && useBuiltIns) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are going to remove these options in Babel 8, and default to .... We can avoid adding them to this new transform.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for reference, #11141 via jsx label

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! I'll delete them for now and default to ... in a separate PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to default to ... already in this PR?

@lunaruan
Copy link
Contributor Author

lunaruan commented Feb 20, 2020

Thanks for the review! I forgot to explain some things that might make this PR and the implementation make a bit more sense. This Babel transform is only meant to be used for React. jsx/jsxs/jsxDEV are not supposed to be used by anything other than the Babel transform. They are meant to be an implementation detail of React and the Babel transform and open to changing in the future, and are setting the stage for other changes. They also have a different function signature than createElement does. We never planned for this to replace the current Babel transform for Preact/Inferno/etc. I proposed some alternate suggestions for your concerns below. LMK if you have any other questions!

We would probably also need pragmaStatic/pramaMulti, pragmaDev and pragmaFrag options.
Or maybe pragma: { single: "React.jsx", multiple: "React.jsxs", dev: "React.jsxDEV", frag: "React.Fragment" }.

Distinguishing between jsx/jsxs/jsxDEV/createElement is an implementation detail for React and will change in the future. Therefore, we don't think that it makes sense to add a pragma option for all of them. In addition, the signature for jsx/jsxs/etc. is different than createElement, so Preact/Inferno/etc. will not be able to use the new transform the way they currently are.

To satisfy the people currently using the pragma with the createElement function signature, what do you think about one of these options?
1.) We could leave the old Babel transform for Preact/Inferno/etc. and use this only for React.
2.) We could have a pragma that, when set, defaults to only using createElement and not jsx/jsxs/jsxDEV

In these examples I'm using the pragma option name rather than autoImport, because I think that it fits better my proposed changes. Keep in mind that some of the examples in the following table aren't valid React imports, but they might be useful for other libraries.

Using source/importSource/pragma to determine how to import React will mostly likely not work for a couple of reasons:
1.) We think the pragma field should not be part of jsx/jsxs/jsxDEV (see above).
2.) There are use cases for all the import options above so we can't get rid of them. (Ex. we want everyone to use named exports but typescript only works with namespace exports etc.) It seems easier for people to specify what kind of auto import that they want rather than trying to figure out what combination of importSource/pragma will get them what they're looking for.

It seems like your concern with the autoImport field is that it is too complicated. In that case, we could simplify it by removing the requires option. What do you this of this alternate proposal?

  • If sourceType is script and autoImport is default/namespace/namedExport, we always go with the var _react = require("react"). If it's none we don't auto import.
  • If sourceType is module, use default/namespace/namedExport/none as specified.

I'm not sure I follow. Why doesn't this work?

const props = { key: "foo" };
React.jsx("div", { ...props}, "Hi");

You'll still see that the second parameter contains a key property, because the "real" key is passed as a different parameter.

What you are suggesting is our end goal; we want to ignore the "props" key and just use the "real" key. However right now, people can use the spread key as an actual key. While we are incrementally deprecating this behavior, we need to preserve the current behavior, which means that <div {...props} key={'hi'} /> (key is 'hi') is different than <div key={'hi'} {...props} /> (key is 'foo'). After we deprecate key spread, this two would be the same and we would remove createElement.

@sebmarkbage
Copy link

Note that part of the reason we want autoImport to be the default is that it prevents sketchy shadowing. E.g. this should not work:

import React from "somethingelse";
<Foo />

OR

function Foo(React) {
  <Foo />
}

The jsx/jsxs/jsxDEV/createElement implementations are just the defaults. The second step is enabling further compiler optimizations by statically knowing that all JSX is going to resolve to a predictable runtime so we can change the behavior of JSX just by knowing what will render it. I.e. similar to babel helpers.

So specifying the actual implementation details in pragmas goes directly against that goal. We want to make the coupling of JSX to a particular implementation stronger to enable compilers to operate on the assumption that it can compile JSX however it wants as long as it knows which runtime it's targeting. E.g. an optimizing Babel plugin can see that this is using importSource "react" and know how to instead use "react/jsx-optimized-helpers" with a completely different implementation, but if it sees importSource "preact" it would ignore it since it doesn't know what that the runtime is compatible with the optimization. Similarly another plugin can do the inverse.

Allowing people to specifying the fine grained implementation details would undermine that ecosystem goal.


// We want to use React.createElement, even in the case of
// jsx, for <div {...props} key={key} /> to distinguish it
// from <div key={key} {...props} />. This is an intermediary
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is necessary?

When I try to reason about it, it seems like the current implementation is the opposite of what should happen. We can reliably tell that <div {...props} key={KEY} /> is supposed to be KEY. In <div key={KEY} {...props} />, we can't tell if the key is KEY or was passed in from the spread props. So to maintain the current behavior while you deprecate, you should be using createElement only in the second case.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For <div key={key} {...props} /> we'll look at props to determine if it has a key and if so override the key passed in. So we preserve the behavior by keeping the runtime behavior + warn and then later change the runtime behavior.

For <div {...props} key={key} /> we don't know if it is <div {...props} key={undefined} /> and we can't distinguish that from <div {...props} /> at runtime because they both pass undefined to the key argument.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense 👍
The comment should probably be updated to mention that it's needed for the undefined case.

Half joking

because they both pass undefined to the key argument.

arguments.length? 😛

@nicolo-ribaudo
Copy link
Member

Thanks for explaining that jsx/jsxs/etc are an implementation detail and are not meant to be something that users should think about. I'm ok with them not being configurable.

I still think that autoImport is unnecessary:

1.) We could leave the old Babel transform for Preact/Inferno/etc. and use this only for React.
2.) We could have a pragma that, when set, defaults to only using createElement and not jsx/jsxs/jsxDEV

I prefer 1. Depending on what the other React-like libraries decide to do (if they want to keep using createElement or not) we'll just have to decide if we want the plugin to be named @babel/transform-react-jsx/compat or @babel/transform-jsx with a mandatory pragma/importSource option.
Anyway, we can defer this discussion to Babel 8.

@nicolo-ribaudo nicolo-ribaudo added this to the v7.9.0 milestone Feb 22, 2020
@nicolo-ribaudo
Copy link
Member

I was thinking about the jsxAutoImport and jsxImportSource options: do you think that they would be needed by React users? Currently, the jsxPragma comment is mostly needed for non-react users.

If you don't think that they would be needed in the real world, we could only allow the option in the config file. If someone asks for them, we can always add them later.

I'm sorry if I'm playing the part of the one who just wants to remove features from the PR, but I'm trying to keep it as simple as possible by avoiding options which might not be necessary 😅

@lunaruan
Copy link
Contributor Author

I still think that autoImport is unnecessary:

Hey! We talked about it a bit and we decided that we don't need the autoImport/jsxAutoImport options after all. We decided that the new transform will always auto import. If users don't want to auto import, they will use the createElement version of the Babel transform instead. What do you think about this:

  1. version is experimental and sourceType is module: we use jsx/jsxs and named exports
  2. version is experimental and sourceType is script: we use jsx/jsxs and require
  3. version is current: we use createElement and don't auto import

jsxVersion can be defined in the docblock to specify whether or not we want to auto import.

(In Babel 8, we can swap the versions so that jsx becomes current and createElement becomes legacy.)

I was thinking about the jsxAutoImport and jsxImportSource options: do you think that they would be needed by React users? Currently, the jsxPragma comment is mostly needed for non-react users.

We do still need jsxImportSource because some users might use React for their app but have a some files that use something other than React elsewhere on the page.

@nicolo-ribaudo
Copy link
Member

I like it!

The only thing that I'm not sure about is the name of the option (the implementation idea is 💯).

I think that version might be confusing because the JSX is the same, it's just the implementation that changes. When seeing a version option, I would expect it to toggle a different flavor of JSX (e.g. facebook/jsx#65). We already give version this meaning to some TC39 proposals.
I propose to use implementation instead.

Also, I don't like "current" because whenever someone sees it they need to check when that option was introduced: the meaning of "current" changes over time. I can think of a few alternatives, but I'm not sure about them. If we can't find anything that satisfies both you and us, we can keep current | experimental.

  • implementation: "createElement" | "jsx": this reflect exactly React's implementation, but it has the problem that doesn't work well with non-React libraries.
  • implementation: "generic" | "specialized": generic is because createElement works for any JSX element, while specialized is because Babel chooses one of jsx, jsxs or jsxDEV.
  • implementation: "generic" | "react": generic is because it's something which works for all the different JSX implementations, while react is because this implementation is react-specific. If other projects add support for the new JSX functions, we could add aliases to react. (I slightly prefer this)

The last thing: from a code point of view, (1) and (2) don't need to be handled separately. You can call addNamed from @babel/helper-module-imports, and it automatically decides if it should use import or require.

@lunaruan
Copy link
Contributor Author

lunaruan commented Feb 26, 2020

The only thing that I'm not sure about is the name of the option (the implementation idea is 💯).

What do you think about runtime: "classic" | "automatic", with the default being "automatic"? We don't want the implementation details to be apparent to the user, so we don't want to explicitly call things createElement or jsx, and one of the main highlights of this transform is the ability to automatically import jsx/jsxs so users don't have to.

We can default to "classic" in 7.9 and "automatic" in 8.0.

The last thing: from a code point of view, (1) and (2) don't need to be handled separately. You can call addNamed from @babel/helper-module-imports, and it automatically decides if it should use import or require.

The reason why we handle this separately is because would prefer to use var React = require("react") over var _jsx = require("react").jsx for requires because we found that it was faster, but we want to use import {jsx as _jsx} from "react" for imports.

@existentialism
Copy link
Member

What do you think about runtime: "classic" | "automatic"

👍

@nicolo-ribaudo
Copy link
Member

Sounds good 👍

@marvinhagemeister
Copy link
Contributor

marvinhagemeister commented Feb 27, 2020

That's super awesome! Simplifying createElement is something we talked about a lot in the Preact team and we came to similar conclusions. We have proposals (non-public) that are very similar to jsx/jxs. Looping in @jviide as he has played around a lot with those ideas in htm which is basically a runtime jsx version based on tagged template literals (works with React too).

About Pragma

Reading through the code I think we can fully support the proposed features. The main reason we need configurable pragmas today is because of React's usage of a default export. We need that to be configurable today is to change React.createElement to just createElement. That's pretty much solved once React switches to named imports. Right now I don't see any reason that would prevent us from supporting jsx, jsxs and jsxDEV.

In our case we'll likely alias jsxDEV to just jsx as all warnings during development can be enabled at runtime by importing preact/debug regardless of how the application was transpiled/bundled. It works by attaching callbacks to various stages of our renderer at runtime, but that's an implementation detail of how Preact works internally.

Auto imports

The only config option I'd maybe like to propose is for the import source itself. We'd like to change import { jsx } from "react" to import { jsx } from "preact". It's not a hard requirement for us as we could always write a custom plugin to swap the import source ourselves. Nonetheless I think it may be a nice thing to have configurable.

Refs

The changes proposed here allow us to simplify our createElement implementation even further by not having to have extra conditions to pull of "special" props like key.

Currently ref needs the same special treatment as key and I'm happy to see that it is not treated as a separate argument to jsx/jsxs here. My hope is that we will keep it as a normal prop, because that would automatically solve the need for a forwardRef-like component.

// Today
const Foo = forwardRef((props, ref) => <div ref={ref) />);

// vs
const Foo = props => <div ref={props.ref} />;

iirc that was part of the original proposal @sebmarkbage made for the future direction of createElement but I can't find it right now in my bookmarks 🙈

Conclusion

All in all we're very excited about these changes. They allow us to simplify our code even further and save more precious bytes. We're fully on board with them and we really appreciate reaching out to us. Great work all around! 🙌 💯

I'll probably have a go at implementing jsx/jsxs/jsxDEV for Preact over the weekend.

@nicolo-ribaudo
Copy link
Member

@lunaruan You can disable the failing test on windows (like it was done at packages/babel-preset-react/test/fixtures/preset-options/development/options.json). Also, please create a windows version of that test.

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Feb 27, 2020

@marvinhagemeister WDYT about the createElement fallback needed by react to warn about key in spread props?

If it's not needed by Preact, I can think of three possibilities:

  • Make it opt-out
  • Automatically disable it if importSource is set (maybe make it opt-in in this case)
  • Always disable it, and instead of having the warning in React we can put it directly in the compiled output (@lunaruan wdyt?):
_jsx("div", { foo: 1, ..._warnIfKey(props) }, key);

// warnIfKey helper implementaition
function _warnIfKey(props) {
  if ("key" in props) console.warn(...);
}

@marvinhagemeister
Copy link
Contributor

@nicolo-ribaudo Personally I'm fine with moving users off of spreading keys, but I'm admittedly a bit scared of the amount of support questions we may get.

I think for us and similar frameworks the most preferable solution would be opt-in. Knowing that, we're very likely at the mercy of the React ecosystem either way, because most warnings/mismatches come from older third party components. Some of those may break unexpectedly (for both React and Preact) if they rely on props.key directly.

Looping in @DanielRosenwasser from the TypeScript team because many Preact users compile jsx via TS directly.

@lunaruan lunaruan force-pushed the react-jsx-next branch 2 times, most recently from cf59c61 to d895cb7 Compare February 27, 2020 21:20
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Jun 22, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: jsx outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: New Feature 🚀 A type of pull request used for our changelog categories PR: Ready For Review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants