Repository navigation
RFC: Plan for custom element attributes/properties in React 19 #11347
Description
This is meant to address #7249. The doc outlines the pros and cons of various approaches React could use to handle attributes and properties on custom elements.
TOC/Summary
- Background
- Proposals
- Option 1: Only set properties
- Pros
- Easy to understand/implement
- Avoids conflict with future global attributes
- Takes advantage of custom element "upgrade"
- Custom elements treated like any other React component
- Cons
- Possibly a breaking change
- Need ref to set attribute
- Not clear how server-side rendering would work
- Pros
- Option 2: Properties-if-available
- Pros
- Non-breaking change
- Cons
- Developers need to understand the heuristic
- Falling back to attributes may conflict with future globals
- Pros
- Option 3: Differentiate properties with a sigil
- Pros
- Non-breaking change that developers can opt-in to
- Similar to how other libraries handle attributes/properties
- The system is explicit
- Cons
- It’s new syntax
- Not clear how server-side rendering would work
- Pros
- Option 4: Add an attributes object
- Pros
- The system is explicit
- Extending syntax may also solve issues with event handling
- Cons
- It’s new syntax
- It may be a breaking change
- It may be a larger change than any of the previous proposals
- Pros
- Option 5: An API for consuming custom elements
- Pros
- The system is explicit
- Non-breaking change
- Idiomatic to React
- Cons
- Could be a lot of work for a complex component
- May bloat bundle size
- Config needs to keep pace with the component
- Pros
- Option 1: Only set properties
Background
When React tries to pass data to a custom element it always does so using HTML attributes.
<x-foo bar={baz}> // same as setAttribute('bar', baz)Because attributes must be serialized to strings, this approach creates problems when the data being passed is an object or array. In that scenario, we end up with something like:
<x-foo bar="[object Object]">The workaround for this is to use a ref to manually set the property.
<x-foo ref={el => el.bar = baz}>This workaround feels a bit unnecessary as the majority of custom elements being shipped today are written with libraries which automatically generate JavaScript properties that back all of their exposed attributes. And anyone hand-authoring a vanilla custom element is encouraged to follow this practice as well. We'd like to ideally see runtime communication with custom elements in React use JavaScript properties by default.
This doc outlines a few proposals for how React could be updated to make this happen.
Proposals
Option 1: Only set properties
Rather than try to decide if a property or attribute should be set, React could always set properties on custom elements. React would NOT check to see if the property exists on the element beforehand.
Example:
<x-foo bar={baz}>The above code would result in React setting the .bar property of the x-foo element equal to the value of baz.
For camelCased property names, React could use the same style it uses today for properties like tabIndex.
<x-foo squidInk={pasta}> // sets .squidInk = pastaPros
Easy to understand/implement
This model is simple, explicit, and dovetails with React’s "JavaScript-centric API to the DOM".
Any element created with libraries like Polymer or Skate will automatically generate properties to back their exposed attributes. These elements should all "just work" with the above approach. Developers hand-authoring vanilla components are encouraged to back attributes with properties as that mirrors how modern (i.e. not oddballs like <input>) HTML5 elements (<video>, <audio>, etc.) have been implemented.
Avoids conflict with future global attributes
When React sets an attribute on a custom element there’s always the risk that a future version of HTML will ship a similarly named attribute and break things. This concern was discussed with spec authors but there is no clear solution to the problem. Avoiding attributes entirely (except when a developer explicitly sets one using ref) may sidestep this issue until the browsers come up with a better solution.
Takes advantage of custom element "upgrade"
Custom elements can be lazily upgraded on the page and some PRPL patterns rely on this technique. During the upgrade process, a custom element can access the properties passed to it by React—even if those properties were set before the definition loaded—and use them to render initial state.
Custom elements treated like any other React component
When React components pass data to one another they already use properties. This would just make custom elements behave the same way.
Cons
Possibly a breaking change
If a developer has been hand-authoring vanilla custom elements which only have an attributes API, then they will need to update their code or their app will break. The fix would be to use a ref to set the attribute (explained below).
Need ref to set attribute
By changing the behavior so properties are preferred, it means developers will need to use a ref in order to explicitly set an attribute on a custom element.
<custom-element ref={el => el.setAttribute('my-attr', val)} />This is just a reversal of the current behavior where developers need a ref in order to set a property. Since developers should rarely need to set attributes on custom elements, this seems like a reasonable trade-off.
Not clear how server-side rendering would work
It's not clear how this model would map to server-side rendering custom elements. React could assume that the properties map to similarly named attributes and attempt to set those on the server, but this is far from bulletproof and would possibly require a heuristic for things like camelCased properties -> dash-cased attributes.
Option 2: Properties-if-available
At runtime React could attempt to detect if a property is present on a custom element. If the property is present React will use it, otherwise it will fallback to setting an attribute. This is the model Preact uses to deal with custom elements.
Pseudocode implementation:
if (propName in element) {
element[propName] = value;
} else {
element.setAttribute(propName.toLowerCase(), value);
}Possible steps:
-
If an element has a defined property, React will use it.
-
If an element has an undefined property, and React is trying to pass it primitive data (string/number/boolean), it will use an attribute.
- Alternative: Warn and don’t set.
-
If an element has an undefined property, and React is trying to pass it an object/array it will set it as a property. This is because some-attr="[object Object]” is not useful.
- Alternative: Warn and don’t set.
-
If the element is being rendered on the server, and React is trying to pass it a string/number/boolean, it will use an attribute.
-
If the element is being rendered on the server, and React is trying to pass it a object/array, it will not do anything.
Pros
Non-breaking change
It is possible to create a custom element that only uses attributes as its interface. This authoring style is NOT encouraged, but it may happen regardless. If a custom element author is relying on this behavior then this change would be non-breaking for them.
Cons
Developers need to understand the heuristic
Developers might be confused when React sets an attribute instead of a property depending on how they’ve chosen to load their element.
Falling back to attributes may conflict with future globals
Sebastian raised a concern that using in to check for the existence of a property on a custom element might accidentally detect a property on the superclass (HTMLElement).
There are also other potential conflicts with global attributes discussed previously in this doc.
Option 3: Differentiate properties with a sigil
React could continue setting attributes on custom elements, but provide a sigil that developers could use to explicitly set properties instead. This is similar to the approach used by Glimmer.js.
Glimmer example:
<custom-img @src="corgi.jpg" @hiResSrc="corgi@2x.jpg" width="100%">In the above example, the @ sigil indicates that src and hiResSrc should pass data to the custom element using properties, and width should be serialized to an attribute string.
Because React components already pass data to one another using properties, there would be no need for them to use the sigil (although it would work if they did, it would just be redundant). Instead, it would primarily be used as an explicit instruction to pass data to a custom element using JavaScript properties.
h/t to @developit of Preact for suggesting this approach :)
Pros
Non-breaking change that developers can opt-in to
All pre-existing React + custom element apps would continue to work exactly as they have. Developers could choose if they wanted to update their code to use the new sigil style.
Similar to how other libraries handle attributes/properties
Similar to Glimmer, both Angular and Vue use modifiers to differentiate between attributes and properties.
Vue example:
<!-- Vue will serialize `foo` to an attribute string, and set `squid` using a JavaScript property -->
<custom-element :foo="bar” :squid.prop=”ink”>
Angular example:
<!-- Angular will serialize `foo` to an attribute string, and set `squid` using a JavaScript property -->
<custom-element [attr.foo]="bar” [squid]=”ink”>
The system is explicit
Developers can tell React exactly what they want instead of relying on a heuristic like the properties-if-available approach.
Cons
It’s new syntax
Developers need to be taught how to use it and it needs to be thoroughly tested to make sure it is backwards compatible.
Not clear how server-side rendering would work
Should the sigil switch to using a similarly named attribute?
Option 4: Add an attributes object
React could add additional syntax which lets authors explicitly pass data as attributes. If developers do not use this attributes object, then their data will be passed using JavaScript properties.
Example:
const bar = 'baz';
const hello = 'World';
const width = '100%';
const ReactElement = <Test
foo={bar} // uses JavaScript property
attrs={{ hello, width }} // serialized to attributes
/>;This idea was originally proposed by @treshugart, author of Skate.js, and is implemented in the val library.
Pros
The system is explicit
Developers can tell React exactly what they want instead of relying on a heuristic like the properties-if-available approach.
Extending syntax may also solve issues with event handling
Note: This is outside the scope of this document but maybe worth mentioning :)
Issue #7901 requests that React bypass its synthetic event system when declarative event handlers are added to custom elements. Because custom element event names are arbitrary strings, it means they can be capitalized in any fashion. To bypass the synthetic event system today will also mean needing to come up with a heuristic for mapping event names from JSX to addEventListener.
// should this listen for: 'foobar', 'FooBar', or 'fooBar'?
onFooBar={handleFooBar}However, if the syntax is extended to allow attributes it could also be extended to allow events as well:
const bar = 'baz';
const hello = 'World';
const SquidChanged = e => console.log('yo');
const ReactElement = <Test
foo={bar}
attrs={{ hello }}
events={{ SquidChanged}} // addEventListener('SquidChanged', …)
/>;In this model the variable name is used as the event name. No heuristic is needed.
Cons
It’s new syntax
Developers need to be taught how to use it and it needs to be thoroughly tested to make sure it is backwards compatible.
It may be a breaking change
If any components already rely on properties named attrs or events, it could break them.
It may be a larger change than any of the previous proposals
For React 17 it may be easier to make an incremental change (like one of the previous proposals) and position this proposal as something to take under consideration for a later, bigger refactor.
Option 5: An API for consuming custom elements
This proposal was offered by @sophiebits and @gaearon from the React team
React could create a new API for consuming custom elements that maps the element’s behavior with a configuration object.
Pseudocode example:
const XFoo = ReactDOM.createCustomElementType({
element: ‘x-foo’,
‘my-attr’: // something that tells React what to do with it
someRichDataProp: // something that tells React what to do with it
});The above code returns a proxy component, XFoo that knows how to pass data to a custom element depending on the configuration you provide. You would use this proxy component in your app instead of using the custom element directly.
Example usage:
<XFoo someRichDataProp={...} />Pros
The system is explicit
Developers can tell React the exact behavior they want.
Non-breaking change
Developers can opt-in to using the object or continue using the current system.
Idiomatic to React
This change doesn’t require new JSX syntax, and feels more like other APIs in React. For example, PropTypes (even though it’s being moved into its own package) has a somewhat similar approach.
Cons
Could be a lot of work for a complex component
Polymer’s paper-input element has 37 properties, so it would produce a very large config. If developers are using a lot of custom elements in their app, that may equal a lot of configs they need to write.
May bloat bundle size
Related to the above point, each custom element class now incurs the cost of its definition + its config object size.
Note: I'm not 100% sure if this is true. Someone more familiar with the React build process could verify.
Config needs to keep pace with the component
Every time the component does a minor version revision that adds a new property, the config will need to be updated as well. That’s not difficult, but it does add maintenance. Maybe if configs are generated from source this is less of a burden, but that may mean needing to create a new tool to generate configs for each web component library.
cc @sebmarkbage @gaearon @developit @treshugart @justinfagnani
Activity
robdodson commentedon Oct 24, 2017
Apologies for the long read, but I wanted to make sure I was thoroughly exploring each option. I don't want to bias things too much with my own opinion, but if I were in a position to choose, I think I'd go with option 3.
Option 3 is backwards compatible, declarative, and explicit. There’s no need to maintain a fallback heuristic, and other libraries already provide similar sigils/modifiers.
worawit15379 commentedon Oct 24, 2017
jeremenichelli commentedon Oct 24, 2017
I'm between option 2 and option 3, I think that React has handled behavior and API changes very well in the past. Introducting warnings and links to docs might serve well to help developers understand what's happening under the hood.
Option 3 looks attractive because of its declarative nature, while reading JSX code new coming developers will know immediately what React will do when rendering the element.
cjorasch commentedon Oct 24, 2017
Comments on option 2
Do consumers of a custom element need to understand this distinction? Or is that only important to the author of the custom element? It seems like the author of the element will need to handle attributes for anything used in HTML (since that is the only way data gets passed from HTML usage) and properties if they want to support complex values or property get/set from DOM. It is even possible an author could have something initially implemented as an attribute and then later add a property with the same name to support more flexible data types and still back the property with a value stored in the attributes.
Naming collisions with future HTMLElement attributes and properties seems like a weakness in the Web Components standards in general since that can lead to errors regardless of the binding approach.
It seems confusing to bind differently based on the value. If the author of the element has not specified a property getter/setter to handle the value then setting the property would cause the element to behave like the value was never specified which might be harder to debug.
Comments on option 3
Another potential con with option 3 is that it requires the consumer of the custom element to know whether the element has implemented something as a property or as an attribute. If you are using a mix of React components and custom elements it could be confusing to set React props using one syntax and custom element properties using a different syntax.
robdodson commentedon Oct 24, 2017
I doubt it's actually a huge issue because, as you pointed out, the element author should define an attribute and property for the underlying value and accept data from both. I would also add that they should keep the attribute and property in sync (so setting one sets the other).
I agree but I'm not sure if this is something React needs to try to work around in their library. It feels like a problem that needs to be solved as part of the custom elements spec. I can see if we can discuss it as part of the upcoming TPAC standards meeting.
I should add, for properties this isn't as bad because the element-defined property will shadow the future property added to HTMLElement. So if you were passing data to a custom element as a js property, it would continue to work. The main issue seems to be around attributes since they are global.
In the case where a custom element is lazy loaded and "upgraded", it will initially have undefined properties. This addresses that use case by making sure those elements still receive their data and they can use it post-upgrade.
It's true that if the author doesn't define a getter/setter for a value this would not be very useful. But it's also not useful to have an
my-attr=[object Object]. And since you don't know if the property is truly undefined or if they definition is just being lazy loaded, it seems safest to set the property.I think you're essentially in the same boat today because there's nothing that forces a custom element author to define an attribute instead of a property. So I could have an element with a properties only API that would not receive any data from React's current system and I would need to know to use
refto directly set the js properties.Because custom elements are meant as a primitive, there's nothing that enforces creating corresponding attributes and properties. But we're trying very hard to encourage doing so as a best practice, and all of the libraries that I know of today create backing properties for their attributes.
[edit]
As you stated in your earlier point:
Because you never know how a user will try to pass data to your element, you end up needing to have attribute-property correspondence anyway. I imagine if option 3 shipped that most folks would just bind everything using the
@sigil because it'd be easiest. That's how I work with custom elements in Vue today since they expose a.propmodifier.jeremenichelli commentedon Oct 24, 2017
That's not something React should worry as Rob said in my opinion, it's the custom element author's responsability to inform the user how the element works.
And it's actually the way that we need to do it today, for example think about the
<video>element, let's say you need to mute it or change the current time inside a component.mutedworks as a boolean attributeFor the current time you need to create a
refpointing to the video element and change the property.Then create an event handler, an instance method and manually set the property to the DOM element.
If you think about it it kinda breaks the declarative model React itself imposes with its API and JSX abstract layer since the
currentTimeit's clearly a state in the wrapper component, with property binding we would still need the event handler but the JSX abstraction model would be more declarative and refs wouldn't be necessary just for this:My point is that whether you are relying on native or custom elements, you still need to know your way around them based on documentation, the difference that in the second case it should come from the custom element's author.
@cjorasch my two cents :)
effulgentsia commentedon Oct 24, 2017
If we were designing this from scratch, without needing to consider backwards compatibility, I think option 1 would be the most idiomatic per React’s "JavaScript-centric API to the DOM".
With regard to server-side rendering, could that problem be solved by providing an API for application code to inform React on how to map custom element properties to attributes? Similar to the maps that React already maintains for platform-defined attributes? This API would only need to be invoked once per custom element name (not for each instance of it), and only for properties that don't follow a straight 1:1 correspondence with their attribute, which should hopefully be relatively rare.
If we're concerned about this being too much of a breaking change though, then I think option 3 is pretty appealing as well. If the sigil signifies a property, I would suggest ".", since that's already JavaScript's property accessor. However, I think it's unfortunate to make every instance of where a custom element is used in an application be responsible for knowing when to use an attribute vs. when to use a property. What I prefer about option 1 is that even if a property to attribute map is needed, that mapping code can be isolated from all the JSX usages.
cjorasch commentedon Oct 24, 2017
Maybe I don't understand the upgrade process. Elements would typically have properties defined as getters/setters in the class prototype. Checking
propName in elementwould return true because of the existence of the getter/setter even if the property value was still undefined. During upgrade do property values get set on some temporary instance and then later get copied to the actual instance once the lazy load is complete?effulgentsia commentedon Oct 24, 2017
Upgrading is the process by which the custom element receives its class. Prior to that, it's not an instance of that class, so the property getters/setters aren't available.
robdodson commentedon Oct 24, 2017
@jeremenichelli
just checked and it also has a corresponding property though it doesn't seem to be documented on MDN :P
Yeah occasionally you'll encounter properties-only APIs on modern HTML elements.
currentTimeupdates at a high frequency so it wouldn't make sense to reflect it to an HTML attribute.Yep there's unfortunately no one-size-fits-all attributes/properties rule. But I think generally speaking you can lean heavily on properties and provide syntax so developers can use attributes in special cases.
jeremenichelli commentedon Oct 24, 2017
@robdodson yeap, I knew about the muted property too 😄 I just used these two to prove that already in the wild there isn't a one-size-fits-all rule as you mentioned.
We will have to rely on documentation on both native and custom elements, so it's something I wouldn't mind for this decision.
While writing the last code snippet I kinda liked the property binding though 💟
robdodson commentedon Oct 24, 2017
@effulgentsia
I think this is already the case today though. Since the major custom element libraries (polymer, skate, possibly others?) automatically create backing properties for all exposed attributes, developers could just use the sigil for every property on a custom element. It would probably be a rare occurrence for them to need to switch to using an attribute.
@cjorasch
RE: upgrade. As @effulgentsia mentioned, it's possible to have a custom element on the page but load its definition at a later time.
<x-foo>will initially be an instance ofHTMLElementand when I load its definition later it "upgrades" and becomes an instance of theXFooclass. At this point all of its lifecycle callbacks get executed. We use this technique in the Polymer Starter Kit project. Kind of like this:In the above example, we won't load the definition for
my-view2until the router changes to it.It's entirely possible to set a property on the element before it has upgraded, and once the definition is loaded the element can grab that data during one of its lifecycle callbacks.
effulgentsia commentedon Oct 24, 2017
If developers started doing that, then how would that differentiate using a property because you "can" from using a property because you "must"? And isn't that a differentiation that's needed for server-side rendering?
262 remaining items
frehner commentedon Oct 17, 2023
@jakearchibald AFAIK this PR #22184 is the base of what was implemented; @josepharhar outlined the implementation in this comment #22184 (comment)
jfbrennan commentedon Oct 17, 2023
Not finding a useful summary in that PR or other linked GH issues.
Two things I saw that did get my attention but wasn't immediately clear what the decision was:
Is this an argument React should even be having? The standard says: "the event handler [content attribute or IDL] is exposed through a name, which is a string that always starts with "on" and is followed by the name of the event for which the handler is intended."
There are no non-event HTML attributes that start with "on" for this reason. Web Component authors must follow this, including custom attributes and custom events, so React simply needs to follow the spec here.
The Custom Element definition may never come (e.g. web server is down, network is slow/stalled, developer failed to include script), but browsers today can and do immediately render yet-to-be-defined custom elements in the DOM because they're designed to be fast and fault tolerant.
Not only that, but Custom Element authors don't always want/need to use shadow DOM, so browsers can and do immediately begin to style custom elements with global styles. Again, the CE definition may still be on its way or a multitude of errors prevent it from ever downloading/evaluating, but that's not a reason to halt rendering what can be rendered.
Lastly, and I'm biased here, but I personally use all of this to my advantage (TAC CSS methodology, Mdash).
For example, basic custom elements like an icon or badge have no need for JavaScript, so I don't create CE definitions for them. But the API of custom elements (i.e. tag and attributes) is better than a bunch of goofy classes, so this is what I do:
Icon component with custom HTML tag, it's 100% vanilla CSS, and no CE definition
And developers use it just like HTML because it is HTML:
Works literally everywhere. Been doing it for 5+ years now in dozens of projects - static HTML, SSR, CSR, with frameworks including React and without - and it's been great. Sometimes these CSS-only elements get new requirements that have to use JavaScript and then I add a CE definition for that behavior, but the markup and styles do not change. It's like progressive enhancement for Custom Elements.
So, please do not break this by waiting for CE to be defined in order to render! Custom HTML like this has been possible for decades and is supported by all browsers for almost just as long. React should follow the browser and every other framework and render what is given to it. Let the CE definition do whatever it does if and when it exists.
jakearchibald commentedon Oct 18, 2023
They 'should' follow this, but there's nothing forcing them. Have you looked at the tests in the PR that shows React's behaviour? It seems pretty reasonable.
Agreed. CSS can provide a nice 'before defined' rendering of a custom element, especially in combination with declarative shadow DOM.
msteller-connyun commentedon Oct 18, 2023
React is the only of the major frameworks which needs an "on" for events.
Existing web component component libraries also don't have an on in their signature:
https://shoelace.style/components/button#events
And I'm pretty sure they will not add an "on" to all their event names because of React.
Or are we talking about if React adds this prefix automatically for web components?
So you would write
<sl-button onSlBlur="...">but it would call the eventsl-blur?jakearchibald commentedon Oct 18, 2023
I believe the event will be
SLBlur. You'd useonsl-blurif you wanted the eventsl-blur.jfbrennan commentedon Oct 18, 2023
@msteller-connyun nothing really to do with React. The event names themselves - native and custom - don't include the "on" prefix. Shoelace event names, Mdash event names, etc. don't include "on" in their event names. The spec defines event handler attribute names start with an "on" prefix "followed by the name of the event". So, using whatever framework (or no framework) when binding events in the template you use the event name prefixed with the framework syntax:
WickyNilliams commentedon Oct 18, 2023
React won't be changing the case at all, it'll use the event name verbatim. Since event names are case sensitive, and react can't know the casing of an event from a custom element, the only safe option is to listen exactly to everything after
on.So in the case of the shoelace example it'll be
onsl-blurAt least this was the case when I lasted tested the experimental build
jfbrennan commentedon Oct 18, 2023
Thanks for pointing to the tests @jakearchibald.
Looks great, custom boolean attribute seems to be missing from the SSR test though.
justinfagnani commentedon Oct 18, 2023
Not that this would not work:
You need to call
addEventListener('sl-blur').The
on...syntax only works if the element specifically listens for an attribute nameonslblur, callsaddEventListener()on itself, and evals the string when events fire.jakearchibald commentedon Oct 19, 2023
@WickyNilliams you are right. I updated my comment which wrongly assumed the event names were lowercased. Here's a relevant test https://github.com/facebook/react/pull/22184/files#diff-db98d094dc4f5b1a9c32b4da4df310370707408a929762868d44bf1acd515923R174
bjankord commentedon Oct 24, 2023
Wild that this issue is 6 years old. Still hoping to see the solution from the experimental branch make its way into the stable release soon.
jfbrennan commentedon Oct 26, 2023
@bjankord 6 years and counting to make the world's most popular web framework compatible with the web. It's Internet Explorer all over again and it's not just WC either #17157
rickhanlonii commentedon Jan 18, 2024
Plan for custom elements support in next stable release
Update: The plan is to release this in React 19, which will have direct support for custom elements. Developers can expect that all tests on custom-elements-everywhere.com will pass by default going forward, like they currently do in the experimental channels. Release date, as well as docs for what's supported, still to be announced.
For information on how it's implemented see @josepharhar's PR implementing the proposal here: #22184
rickhanlonii commentedon Mar 29, 2024
Update: Custom element support has landed in Canary
#27450 has landed, which moves custom element support from
experimentaltocanary(note: as of the time of this comment, the canary hasn't published yet, but it will be published automatically in the next day).This means it will land in the next stable release of React: React 19 🎉
We will include docs for the behavior with the React 19 announcements. Until then, you can read implemented behavior here.