Open robdodson opened 6 years ago
Hi @gaearon, I think this comment from @robdodson would be really helpful:
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:
The workaround for this is to use a ref to manually set the property. el.bar = baz}>
The documentation does mention refs as far as calling methods, but explicitly stating this as regards to passing objects in would go a long way in convincing me there's no desire for lock-in! Or maybe I missed it? I'm referring to this page.
Update: Okay, I guess I can create a PR. Maybe I should have all this time, rather than complaining about it passively.
@josepharhar
Could you elaborate on "artificial delays"? Are you saying that we shouldn't wait for the custom element to be upgraded before throwing all JSX attributes into HTML/element attributes, especially when server rendering?
Yes, you 100% should not be waiting. When an element upgrades is not the concern of a templating library. It's the concern of the application and element author. The element author sets the contract with the application development on when an element needs to be defined. A robust element should be able to respond to being defined late. That means supporting state being set prior to upgrade; so looking for existing state in properties and/or attributes (latter being the most common).
Yes, that's what I'm saying. I don't like blocking attributes from server-side rendering.
Yes, you 100% should not be waiting.
Thanks for this feedback, that knocks out the third option in my doc.
Does anyone have opinions on the second option in my doc? The idea is that when a custom element upgrades, we would remove the HTML attribute from the element (via element.removeAttribute) and then assign it into the object's property in the case that the property-assigning heuristic ('prop' in element) is true. The alternative, which preact does now, is wait until the value being passed into that particular JSX attribute actually changes - then the value will be assigned to the element's property.
We could also do something in between, where we assign the same value to the property on upgrade but don't remove the attribute. Here's a table which may help me describe better:
rendering <my-custom-element myprop={'myvalue'} /> which has an object property for myprop
Preact behavior |
attribute | property |
---|---|---|
renderToString() or render() before upgrade | 'myvalue' |
undefined |
custom element just upgraded | 'myvalue' |
undefined |
value changed: myprop={'mynewvalue'} |
'myvalue' |
'mynewvalue' |
Second option in my doc: Remove attribute and assign to property on upgrade | attribute | property |
---|---|---|
renderToString() or render() before upgrade | 'myvalue' |
undefined |
custom element just upgraded | null |
'myvalue' |
value changed: myprop={'mynewvalue'} |
null |
'mynewvalue' |
"in between" option: Assign to property on upgrade | attribute | property |
---|---|---|
renderToString() or render() before upgrade | 'myvalue' |
undefined |
custom element just upgraded | 'myvalue' |
'myvalue' |
value changed: myprop={'mynewvalue'} |
'myvalue' |
'mynewvalue' |
Should react respond to custom element upgrades at all? Should react remove attributes on upgrade?
a way that works with the whole React feature set (including SSR), we would’ve implemented it years ago
This is a big deal - in an ideal world, we would support custom elements rendering themselves to HTML on the server, using features like declarative shadow DOM (the article even has example code for custom elements to hydrate themselves!). However, it is very unclear how we would do this:
Are you aware of any Web Component Libraries with React that run into issues. For example with https://lit.dev/ or any internal libraries at Google or on the Chromium team?
Thanks for bringing this up, I'll ask around
@josepharhar You bring up a lot of questions but I'll just focus on the first. I think removing attributes and waiting on whenDefined could result in a FOUC. The element would render twice, once without the attributes (on upgrade) and again when React rerenders post-upgrade. Since it's state is different it can likely result in different render results.
If you leave the attributes alone the element has a chance to use those values to correctly render when it upgrades. Then in subsequent React renders (if they are to happen) can use the in
heuristic.
This might sound strange, that your attributes are different than the property values, but this is in fact how most built-in elements work, they do not reflect internal changes onto attributes, attributes reflect initial state.
I think removing attributes and waiting on whenDefined could result in a FOUC.
Could we do the "remove attributes + attach properties" thing on whenDefined?
I think removing attributes and waiting on whenDefined could result in a FOUC.
Could we do the "remove attributes + attach properties" thing on whenDefined?
Yeah I was proposing waiting until after the custom element has been upgraded, then removing the attribute and assigning the same value to the property if our heuristic determines that we should be using object properties.
I could imagine that it could result in something odd happening depending on how the custom element is written, but we would definitely do element.removeAttribute('myprop')
and element['myprop'] = value
in the same javascript task...
@gaearon
Could we do the "remove attributes + attach properties" thing on whenDefined?
You could, does this solve some problem I'm not thinking of though? What do you do with builtin elements here? For example <progress max="50">
, do you remove the attribute and start setting its property? progress will reflect property changes back into its attributes. Would a custom element doing the same mess with diffing or something?
@josepharhar
I'd love to hear the reason why to remove the attribute, it's unclear to me. I can't think of a reason why you need to worry aobut the attr/prop being "out of sync". Many elements will reflect property changes back into the attribute, but some won't. In either case the element is the one that is deciding what the source-of-truth is. I'd be worried that by removing the attribute and the element then reflecting, it might mess with the diffing algo.
A simpler approach for implementing this might be to memoize how you set state on the element the first time. If the element was not upgraded and you used setAttribute
, then continue using setAttribute
for the lifetime of the element. If it was upgraded and you used a property setter, continue using the property setter.
This is nice for you because
1) You don't have to worry about attr/prop being out of sync, you're only setting one of them. and 2) You don't need to treat custom elements special. You don't even have to know that it's a custom element, this heuristic should work the same for builtin elements or HTMLUnknownElements.
If the element was not upgraded and you used setAttribute, then continue using setAttribute for the lifetime of the element. If it was upgraded and you used a property setter, continue using the property setter.
This seems like the same kind of custom element could potentially work with one approach or with other approach but not necessarily both. So there is a risk that you'd either see or not see the issue depending on the timing. We generally prefer things to work the same way regardless of timing. It's better for custom element to not work at all (so that you notice the issue and address it) than for it to work sometimes.
You could, does this solve some problem I'm not thinking of though? What do you do with builtin elements here? For example
I'd love to hear the reason why to remove the attribute, it's unclear to me. I can't think of a reason why you need to worry aobut the attr/prop being "out of sync". Many elements will reflect property changes back into the attribute, but some won't. In either case the element is the one that is deciding what the source-of-truth is. I'd be worried that by removing the attribute and the element then reflecting, it might mess with the diffing algo.
I agree, I think this is a good argument against using removeAttribute. The reason for using removeAttribute is that the state is left kind of funny looking when there is old state left in the attribute and new state in the property, but as you said attributes are (hopefully) more used for initial state only.
However, we still need to know if we should respond to upgrades - see the "Preact behavior" vs "Assign to property on upgrade" tables in my previous comment. (A benefit of not responding to upgrades like preact does is that it takes less code and is probably more performant within the framework...)
A simpler approach for implementing this might be to memoize how you set state on the element the first time. If the element was not upgraded and you used setAttribute, then continue using setAttribute for the lifetime of the element. If it was upgraded and you used a property setter, continue using the property setter.
I agree with Dan, I don't like this because it would significantly change the behavior based on whether the custom element was upgraded the first time you render() it, or if you used SSR - in those cases, we would be sticking to attributes based on timing.
If removeAttribute()
would be used I would first make sure it doesn't exist in the observedAttributes()
list assuming the element is already defined from whenDefined()
.
static get observedAttributes() {
return ['name', 'color'];
}
attributeChangedCallback(name, oldValue, newValue) {
// ...
}
@bahrus
We built web component wrappers around api libraries like eCharts. We are a financial company, so we use lots of grids and charts, so the ability to send objects (not strings) into and out of components (via events) is rather critical for us. Being able to load the components asynchronously is also quite important
I've created full SPA's using Web Components and a good solution I found in my case for this issue was to have the components pass data between themselves outside of any framework. Example, a <json-data>
knows how to pass data down to <data-table>
as an actual object and not a string once the data has downloaded. To keep it generic I define a value
getter and setter on the components for data binding. That way the code works asynchronously and can handle data binding regardless of any main view library (React, Preact, Vue, etc). the main view libraries are then only aware of the element itself and doesn't cause any issues on my end.
I've checked Web Components I previously wrote and tested with React and found one issue that might make sense to add to a checklist of best practices if a topic like that is created in the future.
Basically attributeChangedCallback()
can be fired at the time the element is created but before being attached to the DOM. This isn't an issue when using just including a Web Component on a web page but can cause issues with VDOM for complex components that need to modify the DOM.
The solution is for Web Component Authors to use element.isConnected
prior to internal rendering. I've created an example on CodePen using Leaflet and OpenStreetMap.
The first map renders because it handles element.isConnected
but the second map is blank and errors are logged to console from Leaflet.
The demo allows switching between React and Preact by commenting/uncommenting the desired library to test. Both React and Preact have the same result.
The modified proposal Option 2.SubOption2.NoRemoval does sound better to me than Option 2.SubOption 3. I agree no to removing attributes (with a probably wrong caveat below). It might be a little better than the Preact solution, but...
My concerns are:
How much of a performance impact does it add by adding all these upgrade listeners? Why wait for the upgrade? Why not just pass in the values as soon as it is known? I recognize that there's a possibility some web components might not properly upgrade and incorporate values passed in while unknown. And I understand from parallel work being done by the lit team, that there are some performance benefits of coordinating the order in which components start actively responding to state changes (hence their defer-hydration proposal) -- but as far as I know, no need to throttle when the values should be passed. But if we are imposing any performance penalty to accommodate web components that need a little fine tuning so they can properly absorb properties passed in ahead of time, I'm kind of opposed. If data being passed in is done in a particular order, in order to reduce page reflows, or other beneficial reasons like that, I am in favor of waiting for that reason.
There's only one other reason I can think of for waiting artificially for the element to upgrade, which surely has a non-zero cost, and that's to avoid making the developer have to explicitly say, "hey, I'm an attribute, but my twin sister, with a possibly different name here, is also a (non-string) property that syncs up with the attribute?" Again, if there's any performance penalty from this, why? And some properties won't have corresponding attributes (i.e. not all the sisters are twins)
How are you matching names of attributes with their properties? I suppose it might be safe to assume either a dash or no dash, and that might work in all cases, but I just want to understanding the think / algorithm there. The examples are all lower case, so they match. I hope React isn't imposing that requirement?
I know I'm contradicting what I said yesterday, a bit (we can live with extra attributes, I reluctantly argued), but typically, it won't make much sense for a significant number of properties, but not all the properties, to reflect as attributes during SSR, unless that is the strategy for hydration, which I'm okay with (but some objects / functions can't be serialized? What am I missing? And that seems like a lot of little JSON parsing?). So if those properties aren't getting hydrated via the attribute values, there is a small but significant cost to unnecessary attributes, bandwidth wise, and parsing wise. Not to mention that the elements will look overly busy to developers in the dev tools.
I guess that's my fundamental misunderstanding, and skepticism towards Reacts claim that the whole shebang needs/needed to be understood before implementing piecemeal, obvious (to me) improvements, while waiting for the harder problems to find a resolution. Regardless of hydration approaches, I believe the ability to specify some (non-string, potentially) properties should only be passed in on the client side (either originating on the server, or originating on the client), and not serialized to attributes, was there before SSR, and will remain after SSR. And that's precisely the functionality I've been sorely waiting for all these years. How can we specify such properties? (Sorry, option 3, or at least a core piece of it still sounds better to me).
I'm going to engage in some probably wrong mind reading -- maybe you need attributes for all properties, because they may contain guids that point to some functions / objects that are passed down separately? If that's the case, those attributes, and only those attributes, would make sense to remove. How do we specify that? Again, option 3 sounds like the way.
The modified proposal Option 2.SubOption2.NoRemoval does sound better to me than Option 2.SubOption 3. I agree no to removing attributes (with a probably wrong caveat below). It might be a little better than the Preact solution, but...
I apologize for making this confusing - you're referring to assigning into properties on upgrade or hydration, right?
How much of a performance impact does it add by adding all these upgrade listeners?
Great question
Why wait for the upgrade? Why not just pass in the values as soon as it is known?
We can't use the attribute vs property heuristic until the custom element is upgraded
And I understand from parallel work being done by the lit team, that there are some performance benefits of coordinating the order in which components start actively responding to state changes (hence their defer-hydration proposal)
Could you link to this?
but as far as I know, no need to throttle when the values should be passed
Right, which is why we have determined that we should continue to be setting HTML attributes in SSR and before upgrade
But if we are imposing any performance penalty to accommodate web components that need a little fine tuning so they can properly absorb properties passed in ahead of time, I'm kind of opposed. If data being passed in is done in a particular order, in order to reduce page reflows, or other beneficial reasons like that, I am in favor of waiting for that reason.
If we pass in properties immediately after upgrade instead of waiting for the value itself to change like preact does, then it would make custom elements which are only looking at properties for complex data like objects which doesn't work well in attributes work better, right...?
How are you matching names of attributes with their properties? I suppose it might be safe to assume either a dash or no dash, and that might work in all cases, but I just want to understanding the think / algorithm there. The examples are all lower case, so they match. I hope React isn't imposing that requirement?
I don't think I understand your question... when you say "name", are you talking about the JSX attribute name, which is used as the name of the attribute or property...? Which examples are lower case?
it won't make much sense for a significant number of properties, but not all the properties, to reflect as attributes during SSR
What differentiation between "significant number" and "all" are you making?
but some objects / functions can't be serialized?
In preact, functions won't serialize and objects will serialize to "[Object object]". Today, in react, object also serialize to "[Object object]" and functions get toString()'d. See the renderToString section of my test site.
I believe the ability to specify some (non-string, potentially) properties should only be passed in on the client side (either originating on the server, or originating on the client), and not serialized to attributes, was there before SSR, and will remain after SSR
I suppose that we could treat values which are object specially and wait until custom element upgrade and hydration to assign them...?
I apologize for making this confusing - you're referring to assigning into properties on upgrade or hydration, right?
I guess I question the premise for this.
Great question
If it's a great question, perhaps we should look more closely at option 3? :-)
We can't use the attribute vs property heuristic until the custom element is upgraded
Because option 3 was rejected. Agreed?
Could you link to this?
https://github.com/webcomponents/community-protocols/issues/16 https://github.com/webcomponents/community-protocols/issues/7
If we pass in properties immediately after upgrade instead of waiting for the value itself to change like preact does, then it would make custom elements which are only looking at properties for complex data like objects which doesn't work well in attributes work better, right...?
Due to syntax limitations JSX is imposing, this statement probably makes sense.
I don't think I understand your question... when you say "name", are you talking about the JSX attribute name, which is used as the name of the attribute or property...? Which examples are lower case?
In the example I gave of the UI5 web component, they have a property called valueState and an attribute called value-state, that are intimately tied together. Maybe I'm revealing my ignorance about JSX. How does JSX pair these two differing names together with a single binding? I can imagine some assumptions are made to do that, I just want to understand what those assumptions are.
What differentiation between "significant number" and "all" are you making?
Yes, that was vague. A web component might provide the ability to pass in function callbacks as a property, pass in binary data as another property, pass in another custom element for a third property, etc. I would expect such properties wouldn't have corresponding attributes, (because web components assume attributes can be interpreted in some meaningful way) and it only makes sense to pass these in the client, and SSR would appear to have little to nothing to say about it, little value-add. Support for this feature, it seems to me, should have been added by React years ago, and did not need to wait for SSR to be all figured out, because, as I said, SSR has little to no direct connection to it, as far as I can see. That would have made me a happy camper. But I've been repeatedly reading that it has to wait for a full blown SSR solution, and I've never understood that. Regardless, we are hopefully going to provide support for that with this solution, I'm hoping? Not all properties have corresponding attributes is what I'm really saying, and we need a way to pass things into these properties on the client, using JSX binding. Agreed?
Sorry if this was discussed already, but I don't see it from a quick skim...
According to https://custom-elements-everywhere.com/, Preact's approach distinguishes between a data value that's an object or array from a data value that's a primitive. For the former, it always passes as a property (so on SSR: not rendered as an attribute and instead hydrated as a property on the client), which means that it can set this as a property on the client without caring whether the element is upgraded or not. Meanwhile, for primitive values, it uses the "property if a setter is defined, otherwise attribute" rule, which means it sets as an attribute during SSR or on the client prior to custom element upgrade and as a property after custom element upgrade.
What are the cons to React following this same approach? I think the only cons raised relate to a custom element that has a property setter that expects a primitive value but during upgrade doesn't initialize that property value from the same-named (adjusted for camelCase to kebab-case) attribute value? However, that seems like a bug with the custom element, so not sure how much React should try to accommodate for it, especially when several other popular JS libraries (Preact, Svelte, others) don't.
The significant con is for custom elements that are loaded asynchronously, if the component only renders once, and the component hasn't been upgraded yet, the value is passed in as an attribute string. It might (I don't recall) recover from that on subsequent renders (and I think that was discussed in some of the research @josepharhar has provided.)
Option 2.SubOption2.NoRemoval does, I think, improve this (hooray!) by waiting for the upgrade, with few, if any, downsides that I can see. The only downside might be the performance costs of the awaits (probably small?)
However, that con (elements loaded asynchronously receiving primitive values as attribute values until they are upgraded) is not in any way a con for custom elements that initialize their primitive properties from attribute values. What is a legitimate (non-buggy) example of a custom element with a property setter that can receive a primitive value that doesn't itself initialize that property value from the corresponding attribute value?
Preact (and I think React) works fine with primitive string values. But imagine working with a chart, that needs either JSON attributes, or object properties passed in. I think that's what this whole RFC is about. I should have spelled that out in my brief summary.
So I am taking issue with your statement:
For the former, it always passes as a property
Not so for asynchronously loaded components.
On https://custom-elements-everywhere.com/, it says:
Preact uses a runtime heuristic to determine if it should pass data to Custom Elements as either properties or attributes. If a property is already defined on the element instance, Preact will use properties, otherwise it will fallback to attributes. The exception to this rule is when it tries to pass rich data, like objects or arrays. In those instances it will always use a property.
Is that incorrect?
That statement is correct, but the key word there is "If". For unknown elements still awaiting the download of the JS needed to register the custom element, Preact sees no property already defined, and assumes it is an attribute, and sets that attribute to "[Object object]", so the custom element has no data to work with when it upgrades, at least until the next render.
I meant the part that says:
The exception to this rule is when it tries to pass rich data, like objects or arrays. In those instances it will always use a property.
Oh, true, yes, that probably should have been softened, I agree.
Yeah, that is definitely not what it did when I worked with it (version 8). I have not checked version 10, but I assume from the discussions and @josepharhar's findings, that it is still the case, and that that sentence is inaccurate. Let me review @josepharhar's findings again...
Good point @effulgentsia, @josepharhar, can you confirm whether that statement is accurate or not? Does Preact now always assume anything that isn't a primitive should be assigned as a property, on the client?
https://preactjs.com/guide/v10/web-components/
JSX does not provide a way to differentiate between properties and attributes. Custom Elements generally rely on custom properties in order to support setting complex values that can't be expressed as attributes. This works well in Preact, because the renderer automatically determines whether to set values using a property or attribute by inspecting the affected DOM element. When a Custom Element defines a setter for a given property, Preact detects its existence and will use the setter instead of an attribute.
Assuming complex objects are properties on the client actually makes good sense. I do wonder why Preact didn't adopt that rule (which I'm maybe 90% confident now they didn't).
https://github.com/preactjs/preact/blob/master/src/diff/props.js#L13
@effulgentsia
However, that con (elements loaded asynchronously receiving primitive values as attribute values until they are upgraded) is not in any way a con for custom elements that initialize their primitive properties from attribute values. What is a legitimate (non-buggy) example of a custom element with a property setter that can receive a primitive value that doesn't itself initialize that property value from the corresponding attribute value?
Here is an example:
https://www.dataformsjs.com/examples/web-components-with-react.htm#/data
Both the data page and image gallery do something similar. A <json-data>
component asynchronously downloads data from a JSON service after being created from React and then they use a value
property to pass data from the JSON service to a child Custom Element. In this case a <data-table>
custom element. There is no corresponding value
observable attribute because it wouldn't make sense to include in the API.
Here is the JSX for that part of the app:
function DataPage() {
return (
<json-data url="https://www.dataformsjs.com/data/geonames/countries" load-only-once>
<is-loading template-selector="#loading-screen"></is-loading>
<has-error template-selector="#error-screen"></has-error>
<is-loaded class="flex-col">
// ...
<data-table
data-bind="countries"
highlight-class="highlight"
labels="Code, Name, Size (KM), Population, Continent"
table-attr="is=sortable-table,
data-sort-class-odd=row-odd,
data-sort-class-even=row-even">
</data-table>
</is-loaded>
</json-data>
);
}
Then to see the data table change run this from Dev Tools.
document.querySelector('data-table').value = [{field1:'abc', field2:123}]
@effulgentsia
Here is another demo for your earlier question: https://codepen.io/conrad-sollitt/pen/yLbzraj?editors=1000
See comments at top of the HTML. You can switch between React and Preact by commenting/un-commenting the library you want to test.
The <data-table>
is populated by setting the value
property. Both React and Preact work when using refs and manually setting the value however only Preact works when setting the records directly from JSX like this <data-table value={records}></data-table>
.
For a large comparison of differences see this page: https://josepharhar.github.io/react-custom-elements/
That page was created by @josepharhar for this issue and listed in https://docs.google.com/document/d/1PWm94eCKZ9OBe91X-3ZHIBxn_w0E92VrzSImTEmXPZc/edit
@ConradSollitt: Thank you for those examples. However, for both of them, the value of value
is an array. My suggestion from earlier was to do what https://custom-elements-everywhere.com/ says that Preact does (though apparently, not what Preact actually does), and to always set as a property when the value is an array or object, and to only do the "property if not-SSR-and-element-is-upgraded-and-element-has-a-setter-for-the-property, otherwise attribute" logic for primitive (i.e., string, number, boolean) values. So my question was:
What is a legitimate (non-buggy) example of a custom element with a property setter that can receive a primitive value that doesn't itself initialize that property value from the corresponding attribute value?
Web components that follow best practices do not require upgrading before assigning into properties, on the client. Are we on the same page there?
Thanks for the link! Based on @matthewp’s comment, it sounds like this is an issue people might still run into… Also, I still don’t see how we would ever be setting properties instead of attributes before upgrading with what is on the table.
On the client, Preact only has to wait for the upgrade, because Preact doesn't "own" JSX, hence couldn't make up syntax for us to specify which bindings were meant for string attributes, vs object properties, so it tried its best. React "owns" JSX, but unfortunately seems too reluctant to allow us to specify that explicitly. So yes, within those same constraints, I'm referring to assigning properties, which due to that intransigence means we have to wait. I'm suggesting we are making sacrifices in design to accommodate the rejection of a proposal (option 3) with no explanation (to my knowledge) ever provided. I can live with that, but the truth shall set you free, and I will stop harping on option 3 as soon as I feel satisfied this reality is understood (or perhaps I'm missing something?).
I hear your interest in declaratively differentiating between attributes and properties.
Because option 3 was rejected. Agreed?
Yep
webcomponents/community-protocols#16 webcomponents/community-protocols#7
Thanks for the links! I’m not sure I fully understand how that applies here… should react look for a defer-hydration attribute and avoid setting properties instead of attributes on it until its removed or something…?
In the example I gave of the UI5 web component, they have a property called valueState and an attribute called value-state, that are intimately tied together. Maybe I'm revealing my ignorance about JSX. How does JSX pair these two differing names together with a single binding? I can imagine some assumptions are made to do that, I just want to understand what those assumptions are.
Ah, I wasn’t aware of this concept - what I’m proposing, as well as preact’s current behavior, don’t allow for a way to have one JSX attribute apply to both an attribute named “value-state” and a property named “valueState”.
Yes, that was vague. A web component might provide the ability to pass in function callbacks as a property, pass in binary data as another property, pass in another custom element for a third property, etc. I would expect such properties wouldn't have corresponding attributes, (because web components assume attributes can be interpreted in some meaningful way) and it only makes sense to pass these in the client, and SSR would appear to have little to nothing to say about it, little value-add. Support for this feature, it seems to me, should have been added by React years ago, and did not need to wait for SSR to be all figured out, because, as I said, SSR has little to no direct connection to it, as far as I can see. That would have made me a happy camper. But I've been repeatedly reading that it has to wait for a full blown SSR solution, and I've never understood that. Regardless, we are hopefully going to provide support for that with this solution, I'm hoping? Not all properties have corresponding attributes is what I'm really saying, and we need a way to pass things into these properties on the client, using JSX binding. Agreed?
Yes. Preact does this like you want, right? And if we also reconsidered assigning to a property instead of an attribute when the custom element upgrades, then it will improve the situation when react renders before the custom element is upgraded, right…?
Sorry if this was discussed already, but I don't see it from a quick skim...
According to https://custom-elements-everywhere.com/, Preact's approach distinguishes between a data value that's an object or array from a data value that's a primitive. For the former, it always passes as a property (so on SSR: not rendered as an attribute and instead hydrated as a property on the client), which means that it can set this as a property on the client without caring whether the element is upgraded or not. Meanwhile, for primitive values, it uses the "property if a setter is defined, otherwise attribute" rule, which means it sets as an attribute during SSR or on the client prior to custom element upgrade and as a property after custom element upgrade.
What are the cons to React following this same approach? I think the only cons raised relate to a custom element that has a property setter that expects a primitive value but during upgrade doesn't initialize that property value from the same-named (adjusted for camelCase to kebab-case) attribute value? However, that seems like a bug with the custom element, so not sure how much React should try to accommodate for it, especially when several other popular JS libraries (Preact, Svelte, others) don't.
Based on a test I just made, it looks like preact does not distinguish between strings and objects: https://jsfiddle.net/jarhar/h1qtbwa3/
The significant con is for custom elements that are loaded asynchronously, if the component only renders once, and the component hasn't been upgraded yet, the value is passed in as an attribute string. It might (I don't recall) recover from that on subsequent renders (and I think that was discussed in some of the research @josepharhar has provided.)
Option 2.SubOption2.NoRemoval does, I think, improve this (hooray!) by waiting for the upgrade, with few, if any, downsides that I can see. The only downside might be the performance costs of the awaits (probably small?)
yep!
@effulgentsia Opps, should have paid attention to the primitives part.
Just checked my code and from the first demo I have a <markdown-content>
component. https://www.dataformsjs.com/examples/web-components-with-react.htm#/markdown
It has the following API:
Rather than using a value
HTML attribute it is designed to optionally handle inline markdown:
<markdown-content><script type="text/markdown"># Hello World</script></markdown-content>
One thing to keep in mind when it comes to Web Components is that they are very flexible in how they are defined. Kind of like old jQuery plugins - the same feature set could be defined in completely different manners based on the author and intended audience or usage.
Source code is here: https://github.com/dataformsjs/dataformsjs/blob/master/js/web-components/markdown-content.js
@ConradSollitt: Thank you for those examples. However, for both of them, the value of value is an array. My suggestion from earlier was to do what https://custom-elements-everywhere.com/ says that Preact does (though apparently, not what Preact actually does), and to always set as a property when the value is an array or object, and to only do the "property if not-SSR-and-element-is-upgraded-and-element-has-a-setter-for-the-property, otherwise attribute" logic for primitive (i.e., string, number, boolean) values. So my question was:
What is a legitimate (non-buggy) example of a custom element with a property setter that can receive a primitive value that doesn't itself initialize that property value from the corresponding attribute value?
This sounds promising... assuming the custom element doesn't suffer from this probem: https://github.com/facebook/react/issues/11347#issuecomment-879911564
Let me see if I understand correctly: condition | assign to attribute or property? |
---|---|
not upgraded or SSR, value is object/array/function | property (or nothing if SSR) |
not upgraded or SSR, value is string/number | attribute |
upgraded, value is object/array/function | property |
upgraded, value is string/number, 'propname' in element |
property |
upgraded, value is string/number, !('propname' in element) |
attribute |
@josepharhar Take a look at the comment right above your last one: https://github.com/facebook/react/issues/11347#issuecomment-885208426
In the example I pass a string/primitive but don't include it as an HTML attribute. Reason being I felt it made more sense to support inline markdown rather than passing it as an HTML property.
Of course though being the author of the component if either React or Preact change behavior to always set primitives as HTML attributes then I would just update the component to support it. I checked as Web Components I wrote and that was the only example of a primitive type using a JS property without a related HTML attribute so my guess is it's not common.
Of course though being the author of the component if either React or Preact change behavior to always set primitives as HTML attributes then I would just update the component to support it. I checked as Web Components I wrote and that was the only example of a primitive type using a JS property without a related HTML attribute so my guess is it's not common.
Unless I missed something someone said, I don't think that always using attributes instead of properties for primitive types is on the table...
Unless I missed something someone said, I don't think that always using attributes instead of properties for primitive types is on the table...
Yeah, looks that way. If it were it would be a someone quick fix for Web Component authors that support React and Preact.
Thanks for the link! Based on @matthewp’s comment, it sounds like this is an issue people might still run into… Also, I still don’t see how we would ever be setting properties instead of attributes before upgrading with what is on the table.
I think what @matthewp was saying wasn't too far off from your summary, but a shade different. There are somewhat esoteric ways to make web components upgrade properly, but they do upgrade properly if sufficient care is taken, and that's the responsibility of the web component community to take care of.
I’m not sure I fully understand how that applies here… should react look for a defer-hydration attribute and avoid setting properties instead of attributes on it until its removed or something…?
It seems like there should be a little cross-awareness here of what is happening. No direct impact on React (but React should allow attributes to flow through from the server, which was decided yesterday, so that's great).
Ah, I wasn’t aware of this concept - what I’m proposing, as well as preact’s current behavior, don’t allow for a way to have one JSX attribute apply to both an attribute named “value-state” and a property named “valueState”.
So there might or might not be an issue here. If I'm understanding the direction React is going with SSR -- if it is going full swing towards client-side + server-side components, and quickly deprecating other approaches, I think that is an elegant way of giving developers the ability to shape how much they want to utilize attributes, if at all. Developers can choose to not add any binding on the server-side components, and add to the client-side components, without incurring any unnecessary bandwidth penalties. Likewise, users of libraries like UI5 could specify the correct name for the client-side property vs the server-side attribute, and that wouldn't seem like unnecessary busy work, as the markup will be cleaner and closer to the reality of what's happening, so the extra work seems worth it to me. I hope what I just said is accurate. If it is, then that seems to resolve my concerns here.
If, however, React is going to promote the existing approaches in parallel for a long, sustained time, I think I do see an issue here, which I kind of dismissed as an issue earlier, but is kind of a serious issue -- ending up with two sets of attributes for each property, and the apparent inability to say "I need this property to bind on the client, but please don't send any value as an attribute. It also seems quite clumsy to have to specify two bindings, one for the attribute, one for the property, as it would make tags seem rather cluttered.
It seems to me React should be extra motivated to provide a solution that accommodates web components that use dashes in their attribute names -- being that at least one member of the team thinks all non global attributes should have dashes. Right?
@bahrus Do you know of a scenario where dashes do not work with attribute names for React with Web Components? I am not a team member or employee of FB but since I author Web Components (and React Components) I'm interested in the issue and if there are problems I'm unaware of. The demo from my earlier comment https://github.com/facebook/react/issues/11347#issuecomment-885157069 uses React (Preact version also exists of the demo and they share JSX) and many of the Web Components in the demo contain dashes and I found no issues.
@ConradSollitt
I think possibly that where we are at this point, Option2.SubOption2.NoRemove, to some degree we are moving on from "does not work" to the question of "does it work really well?"
@josepharhar's comment:
Ah, I wasn’t aware of this concept - what I’m proposing, as well as preact’s current behavior, don’t allow for a way to have one JSX attribute apply to both an attribute named “value-state” and a property named “valueState”.
...leads me to believe we have an issue, where things may "work", but if you force your browser to throttle things and play things in very slow motion, I think there may be some issues, or if we ask the question "have we optimized the way this work sufficiently?" the answer might be no. I really want to endorse a solution, as I want this issue resolved, so much so that I will be happy with some compromises. But I'm trying to bring up issues, in case they are issues that haven't been considered yet.
One unresolved question is whether SSR should behave kind of identically to CSR. Your example demo is CSR, where there is a tiny concern I have. My last two posts were actually more focused on SSR, where those concerns grow larger.
@josepharhar seems quite willing to engage in considering options where SSR behaves differently from CSR. So that appears to be on the table still, which is a good thing.
For example, having properties with complex objects do nothing during SSR but only pass in property values on the client is a reasonable choice to make, if we must rule out option 3 and go with one and only one behavior for this scenario.
@josepharhar is considering that possibility here Unfortunately, this blocks out the ability, to sometimes say "hey, I would actually like to JSON.stringify this property during SSR if I may, because I've found that in this particular scenario it hydrates more quickly. Oh, and also, strange as it sounds, I have style associated with the presence of the attribute associated with that property. Likely scenario? Probably not. But one way or the other, Option 2 is going to block developers from making certain choices, I think, some of them reasonable choices.
It seems to me React really wants to "keep things simple" and have only one binding syntax for developers to have to grapple with. I understand the appeal. It's like a car with automatic transmission. Even though the reality is that behind the scenes, there are still constantly shifting gears, the driver can be oblivious to this fact. But even cars still have to have the ability to go in reverse, and shifting into higher gears when going downhill for a sustained period of time. In my mind, the developer, and the user, would still benefit from some (maybe not frequently used) ways of "shifting gears" when absolutely necessary. One such case, I think, is working with compound name properties, which link up with attributes with dashes in the names. This ought to be a high priority concern for the React team, if they are indeed sincere about their concerns about us using future global attribute names, and their desire to see more hyphens in custom element related aspects, like attributes and event names.
Let's start with CSR, which we both understand more deeply as both of us have used that exclusively, I think.
Say I have a property, irrationalNumber, with corresponding attribute "irrational-number" which will require rapid updates on the client, being passed in different values from a React component. Typical values might be Math.PI, or Math.E. There's a significant cost to using attributes for doing this, as this would require repeated float.parse's, which has a cost. Why incur that cost, if the React component already has the float number in memory?
But suppose I need styling to be affected by whether or not a value has been passed in.
So I will need to design my web component in such a way that the web component will reflect the value to the attribute, when the value of the property is passed in. This behavior, it has been rightly pointed out, is not typical behavior for native, built-in elements. Web component authors are encouraged to use pseudo-state, which aligns better with how native-born elements behave, but which doesn't yet have cross-browser support.
This scenario is actually much more important when it comes to SSR (but easier to understand perhaps with CSR).
So during SSR rendering of the component, it should render:
<x-foo irrational-number=3.1>
...my light children
</x-foo>
Or with CSR, during the first round of rendering, before the component has upgraded, it would also be helpful to add that attribute, so styling can immediately kick in. (If the upgrade has already taken place, the component could be passed in the property value, and reflect to the attribute / pseudo-state as needed, so React doesn't need to care about this at that point).
But on the client, during hydration, or (after waiting for the element to upgrade due to the constraints option 2 imposes) we need to pass in myXFoo.irrationalNumber = 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679
It appears I cannot express this with JSX as it stands, according to @josepharhar. Or I can, by using duplicate bindings
<x-foo irrationalNumber={myNumber} irrational-number={myNumber}/>
which would produce a very confusing situation for the web component developer -- having to discard approximate or expensive string attribute updates when more precise property values are being passed in. And on the SSR side, it means possibly duplicate attributes being sent.
Which all seems ironic -- React's team claims part of their foot dragging is because of lack of enthusiasm for supporting custom attributes without dashes, and yet, not expanding JSX appears to be pushing developers hard in the direction of avoiding dashes in attribute names.
But to be fair, possibly React's client-side vs server-side components might be a good solution, if it is expected that developers will typically duplicate their JSX syntax across both files, with minor variations, and if other solutions will quickly be deprecated. If either of those if's break down, then I think Option 2 is insufficient, and failure to enhance JSX would mean the React team is guilty of saying one thing, and yet doing another. Still, if it's an improvement over what we have (which Option2.SubOption2.NoRemove appears to be) I'll take it. Just saying.
I should add, it might be possible to solve the issue of compoundName properties pairing with dashes in attribute names with Option2, if JSX makes some assumptions, does a little fishing around for the corresponding attribute name, or something. But that should be spelled out.
For some background to @bahrus's comment, part of the challenge with respect to whether multiword attribute names have dashes or not comes from inconsistencies within HTML itself as well as across web component helper libraries. For example:
contentEditable
property is contenteditable
while its attribute for the acceptCharset
property is accept-charset
.Meanwhile, most web component helper libraries allow you to override the default mapping for any given property, and so, for example, some PatternFly elements prefix some attribute names, but not the corresponding property names, with "pfe-".
I can think of 3 options for how React might want to deal with this:
react:htmlAttributes
. This would be an object keyed on the JSX attribute name. It could be empty or only include the JSX attributes which need non-default behavior. The value for each JSX attribute name could be either true, false, or a string. True could mean to always set it as an attribute (never check for a property). False could mean to always set it as a property (never render or set it as an attribute). And a string would mean to set it as a property (using the JSX attribute name) on the client, but during SSR to render it as an attribute with the name specified in that string. For any JSX attributes not in this object, apply the default behavior (1 or 2 above). Developers wouldn't need to know or care about the react:htmlAttributes
JSX attribute until they run into a problem with React's default behavior, and then for those cases, this would give them a way to resolve such a problem.React's maintainers might dislike this comment's item 3 for some of the same reasons they dislike this issue's original "Option 3". Especially if there's no prior use for a react:
namespace. However, there is a prior suggestion for moving React's other special attributes (like key
and ref
) to a namespace.
Thanks for the feedback!
I don't have any feedback about the "valueState" vs "value-state" problem right now.
I'd like to bring up another topic: custom event listeners.
Preact currently implements event listeners by doing this:
This behavior is applied to all elements in Preact, not just custom elements. In React, I propose we do something very similar but only for custom elements where the JSX name is not already a reserved React event handler name - those should still go through the React event system.
I also think it would make sense to look at the type of the value - if it is a function, then do the Preact logic. If it's something else, then fall back to the regular attribute vs property behavior. I imagine there are attributes or properties out there which happen to start with "on", right...?
My view:
Hopefully someday React will incorporate (a little of?) Option 3, including the ability to specify whether an JSX attribute is a client-side-only property, and that will allow for properties that start with "on".
Initial implementation work has started: https://github.com/facebook/react/pull/22184. Huge thanks @josepharhar for driving this.
We've merged https://github.com/facebook/react/pull/22184 into main
behind a flag.
This means that the new behavior, described in https://github.com/facebook/react/pull/22184#issuecomment-987510813, has landed into the @experimental
npm release of react
and react-dom
. Here is an example showing that assigning properties and event handlers works now: https://codesandbox.io/s/shy-tdd-8b4tq?file=/src/App.js
The behavior has landed in the @experimental
builds, but we haven't yet reached a final decision on the behavior for the 18 release. You can start using it right away in the @experimental
channel, but whether it lands in @18
or gets deferred to another major release depends on the amount of real-world testing the @experimental
build will receive from the community. If we can get apps using WCs widely try it in practice in the new few weeks and provide feedback on the new behavior in the @experimental
build, we might be able to include it in 18. If we don't get enough feedback by that point, we might have to keep it @experimental
-only until we have more confidence that the new behavior doesn't have big flaws.
If we end up deferring it, this would be understandably frustrating, so I want to clarify that:
@experimental
releases.javascript:
URLs (for security), removing input attribute/property mirroring (to fix bugs), removing legacy context (to reduce bundle size), removing object-assign
polyfill (to reduce bundle size), removing deprecated lifecycle names (which we initially planned to do in 17, then moved to 18, and then to 19). So this isn't a case of us "not caring" about some feature (we care about all of these!) but about making sure that the grouping makes sense and that any churn gets batched.So please give the @experimental
release a try and let us know your experiences!
Nice,thank you! ——weihong
also can i just say @josepharhar has done some heroic effort here in scoping this out, weighing tradeoffs, researching the landscape, proposing the direction and implementing it. huge props 💜
@gaearon thanks for the update! And thanks so much for your hard work @josepharhar!
For the example you linked above, do you know why the event handler doesn't appear to be called? Is it the demo, or something in the event support?
This is great news. Thanks for the effort to everybody involved.
About the example code; the event handler, thus the console.log, is never called on my end too.
Kim
ons. 8. dec. 2021 kl. 18.30 skrev Justin Fagnani @.***>:
@gaearon https://github.com/gaearon thanks for the update! And thanks so much for your hard work @josepharhar https://github.com/josepharhar!
For the example you linked above, do you know why the event handler doesn't appear to be called? Is it the demo, or something in the event support?
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/facebook/react/issues/11347#issuecomment-989021700, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABBO5LVKET3HAG5MSOG7ZALUP6I4VANCNFSM4EAPOZTQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.
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
When React tries to pass data to a custom element it always does so using HTML attributes.
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:
The workaround for this is to use a
ref
to manually set the property.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:
The above code would result in React setting the
.bar
property of thex-foo
element equal to the value ofbaz
.For camelCased property names, React could use the same style it uses today for properties like
tabIndex
.Pros
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.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:
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.
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.
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:
In the above example, the @ sigil indicates that
src
andhiResSrc
should pass data to the custom element using properties, andwidth
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:
Angular example:
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:
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
.However, if the syntax is extended to allow attributes it could also be extended to allow events as well:
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
orevents
, 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:
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:
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