-
Notifications
You must be signed in to change notification settings - Fork 568
RFC: createElement changes and surrounding deprecations #107
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,325 @@ | ||||||
| - Start Date: 2019-02-20 | ||||||
| - RFC PR: (leave this empty) | ||||||
| - React Issue: (leave this empty) | ||||||
|
|
||||||
| # Summary | ||||||
|
|
||||||
| NOTE: This proposal will sound scary. Keep in mind, while reading this, that the actual upgrade path should be very simple for most people since the deprecated things are mostly edge cases and any common ones can be codemodded. | ||||||
|
|
||||||
| This proposal simplifies how React.createElement works and ultimately lets us remove the need for forwardRef. | ||||||
|
|
||||||
| - Deprecate "module pattern" components. | ||||||
| - Deprecate defaultProps on function components. | ||||||
| - Deprecate spreading `key` from objects. | ||||||
| - Deprecate string refs (and remove production mode `_owner` field). | ||||||
| - Move `ref` extraction to class render time and `forwardRef` render time. | ||||||
| - Move `defaultProps` resolution to class render time. | ||||||
| - Change JSX transpilers to use a new element creation method. | ||||||
| - Always pass children as props. | ||||||
| - Pass `key` separately from other props. | ||||||
| - In DEV, | ||||||
| - Pass a flag determining if it was static or not. | ||||||
| - Pass `__source` and `__self` separately from other props. | ||||||
|
|
||||||
| The goal is to bring element creation down to this logic: | ||||||
|
|
||||||
| ``` | ||||||
| function jsx(type, props, key) { | ||||||
| return { | ||||||
| $$typeof: ReactElementSymbol, | ||||||
| type, | ||||||
| key, | ||||||
| props, | ||||||
| }; | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| # Motivation | ||||||
|
|
||||||
| In React 0.12 time frame we did a bunch of small changes to how `key`, `ref` and `defaultProps` works. Particularly, they get resolved early on in the `React.createElement(...)` call. This made sense when everything was classes, but since then, we've introduced function components. Hooks have also make function components more prevalent. It might be time to reevaluate some of those designs to simplify things (at least for function components). | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| Element creation is a hot path because it is used a lot but also because it is always recreated in rerenders. | ||||||
|
|
||||||
| `React.createElement(...)` was never intended to be the implementation of JSX but was the best we could do with tooling at the time. It was intended as what you might write manually (if you didn't want to use the createFactory form). The alternatives never provided enough value to warrant rolling them out everywhere. It has a number of issues: | ||||||
|
|
||||||
| - We need to do a dynamic test against a component if it has a `.defaultProps` during every element creation call. This can't be optimized well because the function it is called within is highly megamorphic. | ||||||
| - `.defaultProps` in element creation doesn't work with `React.lazy` so in that case we also have to check for resolving defaultProps in the render phase too, and means that the semantics are inconsistent anyway. | ||||||
| - Children are passed as var args and we have to patch them onto props dynamically instead of statically knowning the shape of the props at the callsite. | ||||||
| - The transform uses `React.createElement` which is a dynamic property look up instead of a constant closed over module scope. This minimizes poorly and takes a little cost to run. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately this currently only applies when not also transformed to CommonJS. Both SWC and Babel will use It works as intended without any module transforms though. |
||||||
| - We don't know if the passed in props is a user created object that can be mutated so we must always clone it once. | ||||||
| - `key` and `ref` gets extracted from JSX props provided so even if we didn't clone, we'd have to delete a prop, which would cause that object to become map-like. | ||||||
| - `key` and `ref` can be spread in dynamically so without prohibitive analysis, we don't know if these patterns will include them `<div {...props} />`. | ||||||
| - The transform relies on a the name `React` being in scope of JSX. I.e. you have to import the default. This is unfortunate as more things like Hooks are typically used as named arguments. Ideally, you wouldn't have to important anything to use JSX. | ||||||
sebmarkbage marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| - The transform relies on a the name `React` being in scope of JSX. I.e. you have to import the default. This is unfortunate as more things like Hooks are typically used as named arguments. Ideally, you wouldn't have to important anything to use JSX. | |
| - The transform relies on a the name `React` being in scope of JSX. I.e. you have to import the default. This is unfortunate as more things like Hooks are typically used as named imports. Ideally, you wouldn't have to important anything to use JSX. |
sebmarkbage marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of importing import {jsx} from "react", have you considered injecting the jsx?
function Foo() {
return jsx => jsx('div', ...);
}or
function Foo(props, jsx) {
return jsx('div', ...);
}Also, have you considered calling it h, instead of jsx, like Vue, Preact and the rest of the community does?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jsx makes more sense to me. The rest of the community isn’t strictly true - Inferno uses createVNode, for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trueadm Inferno is 100x less popular than Vue and Preact. And there are at least 10 more libs that use h.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@streamich Where did you get the 100x figure from? Why are you even comparing popularity for? It's completely irrelevant for what we're looking for. Inferno influenced many of today's frameworks and libraries in the ecosystem - including React, Preact and Vue 3. That accounts for much more than Github stars or npm downloads in my eyes. My reference to Inferno was specific to this RFC too. createVNode in Inferno was designed purely for JSX compilation, not for users to hand-write. For those wanting to write UIs without JSX, the option to use something like hyperscript is still available (it just wraps around internal APIs).
There is a reason why h is used, and that's because it was originally short for hyperscript which has a specific API that was adopted by virtual DOM frameworks. https://github.com/Raynos/virtual-hyperscript. This differs from what this RFC is trying to do, as the signature will no longer be hyperscript or even hyperscript-like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trueadm Your point about the different signature is good, other points not so much.
But is is the signature that much different? The signature of various h across the libraries are quite a bit different, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@streamich The core team aren't looking to leverage hyperscript though. We're looking for a better signature for creating React elements, specifically from JSX input. I don't see jsx as something people will be manually using, which was why hyperscript came around in the first place (to be a better alternative to createElement for building UIs in non-JSX libraries).
There's nothing stopping people from creating a wrapping library that offers an API like h that wraps around this new API. Like I said in my original point, there's a reason why not every library uses h, and my given example was Inferno. It was nothing to do with popularity, it was because of consumption. You never write createVNode in Inferno, it's got an API very much like the one proposed in this RFC, in where it's purely created from JSX compilation. You'd use h or createElement in Inferno, if you were writing the nodes manually by hand (without JSX). Those APIs are just wrappers around createVNode. I'm suggesting the exact same hypothesis here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this definitely warrants it's own function name. We will want to export an h() shim that calls jsx() internally if this moves forward. h() would imply hyperscript compat and I don't think it would be a good idea to break that assumption.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The core team aren't looking to leverage hyperscript though. We're looking for a better signature for creating React elements, specifically from JSX input.
OK, I see your point and it makes perfect sense. And I see you proposed signature with flags, which also makes sense:
It's just from reading this RFC, the proposed signature
is pretty much what some libraries call h, with maybe exception for third key argument.
But if you add flags there, then you are right, then it definitely does not make sense to call it h,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes for a more consistent API though. It doesn't matter what at what particular elementType I'm looking at currently. All I know is that a static property called defaultProps is responsible for default values.
Makes (human and machine) parsing of a given component definition much easier: "Just" scan for defaultProps.
This is also tricky for doc generators like react-docgen. It currently only supports defaultProps if destructuring is happening in the function signature. This might lead to an opinionated pattern how function component signatures should look like e.g.
function Component(props) {
const { optionalProp = 'defaulted' } = props;
}does not work currently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eps1lon As far as I know the plan is to deprecate class-based components entirely in the long run. Viewed from that perspective the decision to remove defaultProps or the other things in this RFC make a lot more sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deprecate=split them into own module
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we can't meaningfully "deprecate" class components (and don't intend to) in the observable future. However we do want to de-emphasize them if the Hooks adoption is successful and growing. Then simplifying the modern API at the cost of some clumsiness in the de-emphasized API seems justified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my reading, the React Fire Ticket #13525 seemed to suggest that destructuring is not really a preferred pattern here (classname being renamed to class, giving destructuring of class a beginner-unfriendly syntax).
While I'm all in favor of default props being deprecated in favor of default props & destructuring there seems to be a slight conflict here in terms of simplicity & education of new developers.
I have no real opinion of either way, just wanted to point it out early in the thought process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea. Tbh I’ve grown to favor keeping className instead of class for similar reasons.
The plan for Fire is more lava than set in stone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about react:key? It clarifies that it’s a React-specific prop that’s not passed to the component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does requires a change in React since this.refs is a sealed object in DEV unless we use string refs. Which means the codemod can't be applied in versions where string refs are deprecated (we already have a deprecation warning in StrictMode trees). Unless we change the codemod to use
ref={(current) => {
if (process.env.NODE_ENV !== 'production') {
if (Object.isSealed(this.refs)) {
this.refs = {}
}
}
this.refs.stringRef = current;
}}instead: https://codesandbox.io/s/correct-string-refs-codemod-yejy2e?file=/src/App.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can also just unseal it in versions where you're supposed to apply the codemod.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not just about unsealing I realized. React classes share the same object in this.refs until we attach string refs. So the proper codemod would apply
const emptyRefsObject = new React.Component().refs;
...
ref={(current) => {
if (this.refs === emptyRefsObject) {
// This is a lazy pooled frozen object, so we need to initialize.
this.refs = {};
}
if (current === null) {
delete this.refs.stringRef;
} else {
this.refs.stringRef = current;
}
}}
to ensure the same behavior as string refs.
Or we would ensure that each new react class component instance has its own refs object by default



There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.