React: RFC: Plan for custom element attributes/properties in React 18

Created on 24 Oct 2017  Â·  129Comments  Â·  Source: facebook/react

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



    • 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



    • 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



    • 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



    • 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



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 = pasta

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.

<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="[email protected]" 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

DOM Discussion

Most helpful comment

Hey folks, in the meantime while we wait, I created a shim for wrapping your web component in React https://www.npmjs.com/package/reactify-wc

import React from "react";
import reactifyWc from "reactify-wc";

// Import your web component. This one defines a tag called 'vaadin-button'
import "@vaadin/vaadin-button";

const onClick = () => console.log('hello world');

const VaadinButton = reactifyWc("vaadin-button");

export const MyReactComponent = () => (
  <>
    <h1>Hello world</h1>
    <VaadinButton onClick={onClick}>
      Click me!
    </VaadinButton>
  </>
)

I hope this proves helpful

(This is my first foray into OSS, and one of the first open-sourcing of something out of my office. constructive criticism is more than welcome 😄 )

All 129 comments

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.

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.

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.

Comments on option 2

Developers might be confused when React sets an attribute instead of a property depending on how they’ve chosen to load their element.

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.

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.

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.

Do consumers of a custom element need to understand this distinction? Or is that only important to the author of the custom element?

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).

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.

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.

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.

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.

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.

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 ref to 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:

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.

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 .prop modifier.

it requires the consumer of the custom element to know whether the element has implemented something as a property or as an attribute

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.

muted works as a boolean attribute

render() {
  return (
    <div className="video--wrapper">
      <video muted={ this.state.muted } />
    </div>
  );
}

For the current time you need to create a ref pointing to the video element and change the property.

render() {
  return (
    <div className="video--wrapper">
      <video ref={ el => this.video = el } muted={ this.state.muted } />
    </div>
  );
}

Then create an event handler, an instance method and manually set the property to the DOM element.

onCurrenTimeChange(e) {
  this.video.currentTime = e.value;
}

If you think about it it kinda breaks the declarative model React itself imposes with its API and JSX abstract layer since the currentTime it'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:

render() {
  return (
    <div className="video--wrapper">
      <video muted={ this.state.muted } @currentTime={ this.state.currentTime } />
    </div>
  );
}

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 :)

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.

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.

Maybe I don't understand the upgrade process. Elements would typically have properties defined as getters/setters in the class prototype. Checking propName in element would 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?

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.

@jeremenichelli

muted works as a boolean attribute

just checked and it also has a corresponding property though it doesn't seem to be documented on MDN :P

For the current time you need to create a ref pointing to the video element and change the property.

Yeah occasionally you'll encounter properties-only APIs on modern HTML elements. currentTime updates at a high frequency so it wouldn't make sense to reflect it to an HTML attribute.

My point is that wether you are relying on native or custom elements, you still need to know your way around them based on documentation

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.

@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 💟

@effulgentsia

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.

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 of HTMLElement and when I load its definition later it "upgrades" and becomes an instance of the XFoo class. At this point all of its lifecycle callbacks get executed. We use this technique in the Polymer Starter Kit project. Kind of like this:

<app-router>
  <my-view1></my-view1>
  <my-view2></my-view2>
</app-router>

In the above example, we won't load the definition for my-view2 until 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.

developers could just use the sigil for every property on a custom element

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?

If developers started doing that, then how would that differentiate using a property because you "can" from using a property because you "must"?

Sorry, maybe I phrased that wrong. I meant that developers would likely use the sigil because it would give the most consistent result. You can use it to pass primitive data or rich data like objects and arrays and it'll always work. I think working with properties at runtime is generally preferred to working with attributes since attributes tend to be used more for initial configuration.

And isn't that a differentiation that's needed for server-side rendering?

It might be the case that on the server the sigil would fallback to setting an attribute.

It might be the case that on the server the sigil would fallback to setting an attribute.

I don't think that would work if the reason for the sigil is that it's a property that doesn't exist as an attribute, such as video's currentTime.

differentiate using a property because you "can" from using a property because you "must"

I think this differentiation is important, because there's entirely different reasons for choosing to use an attribute or property as an optimization (e.g., SSR preferring attributes vs. client-side rendering preferring properties) vs. something that exists either as only an attribute or only a property.

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?

To be more specific, I'm suggesting something like this:

ReactDOM.defineCustomElementProp(elementName, propName, domPropertyName, htmlAttributeName, attributeSerializer)

Examples:

// 'muted' can be set as either a property or an attribute.
ReactDOM.defineCustomElementProp('x-foo', 'muted', 'muted', 'muted')

// 'currentTime' can only be set as a property.
ReactDOM.defineCustomElementProp('x-foo', 'currentTime', 'currentTime', null)

// 'my-attribute' can only be set as an attribute.
ReactDOM.defineCustomElementProp('x-foo', 'my-attribute', null, 'my-attribute')

// 'richData' can be set as either a property or an attribute.
// When setting as an attribute, set it as a JSON string rather than "[object Object]".
ReactDOM.defineCustomElementProp('x-foo', 'richData', 'richData', 'richdata', JSON.stringify)

For something that can only be a property (where htmlAttributeName is null), SSR would skip over rendering it and then hydrate it on the client.

For something that can only be an attribute (where domPropertyName is null), React would invoke setAttribute() as currently in v16.

For something that can be both, React could choose whatever strategy is most optimal. Perhaps that means always setting as a property on client-side, but as an attribute server-side. Perhaps it means setting as an attribute when initially creating the element, but setting as a property when later patching from the vdom. Perhaps it means only setting as an attribute when the value is a primitive type. Ideally, React should be able to change the strategy whenever it wants to as an internal implementation detail.

When React encounters a prop for which defineCustomElementProp() hasn't been called and which isn't defined by the HTML spec as a global property or attribute, then React can implement some default logic. For example, perhaps:

  • In version 17, maintain BC with v16 and set as an attribute.
  • In version 18, assume that it can be either and follow the most optimal strategy for that.

But in any case, by keeping this a separate API, the JSX and props objects are kept clean and within a single namespace, just like they are for React components and non-custom HTML elements.

Sorry for the excessive comments, but I thought of another benefit to my proposal above that I'd like to share:

Those ReactDOM.defineCustomElementProp() calls could be provided in a JS file maintained by the custom element author (in the same repository as where the custom element is maintained/distributed). It wouldn't be needed for custom elements with a strict 1:1 correspondence of property/attribute, which per this issue's Background statement is the recommendation and majority case anyway. So only custom element authors not following this recommendation would need to provide the React integration file. If the author doesn't provide it (e.g., because the custom element author doesn't care about React), then the community of people who use that custom element within React apps could self-organize a central repository for housing that integration file.

I think the possibility of such centralization is preferable to a solution that requires every user of the custom element to always have to be explicit with a sigil.

Option 3 would be my preferred but that's a huge breaking change... What about the inverse? Attributes have a prefix not props?

Sigils in React, i don't know how i feel about it. The JSX spec should be regarded as universal, not overly reliant or dependent on browser specifics, especially not on irregularities due to backward compatibility. obj[prop] = value and obj.setAttributes(props, value) behaving different is unfortunate but looking at the browsers api as a whole, not a surprise. @ : [] would make a implementation detail leak to the surface and contradict the javascript centric approach. So unless we have a spec that does the following i think it's a bad idea: const data = @myFunction // -> "[object Object]"

If i have to rely on a web component, i'd be glad if the semantics are hidden away from React and JSX as well as making sure they do not introduce breaking changes. From all the options, leaving ref => ... in place seems to be favourable to me. ref is specifically designed to access the object. And at least the developer knows exactly what's going on, there's no sigil-leak, neither new attributes that could break existing projects.

@LeeCheneler

Option 3 would be my preferred but that's a huge breaking change... What about the inverse? Attributes have a prefix not props?

Why would it be a breaking change? The current behavior of attributes being the default would remain. The sigil would be opt-in and developers would use it to replace the spots in their code where they currently use a ref to pass data to a custom element as a JS property.

@drcmda

neither new attributes that could break existing projects.

Can you clarify what you meant by this?

FYI for anyone following the discussion, I've updated the RFC with a 5th option suggested by members of the React team.

@robdodson

I was referring to this:

Option 4: Add an attributes object
Cons
It may be a breaking change

Option 5 seems safest for us. It lets us add the feature without having to make a decision about “implicit” API right now since the ecosystem is still in the “figuring it out” phase. We can always revisit it in a few years.

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.

My impression is that custom element users in React will eventually want to wrap some custom elements into React components anyway for app-specific behavior/customizations. It is a nicer migration strategy for this case if everything already is a React component, e.g.

import XButton from './XButton';

and that happens to be generated by

export default ReactDOM.createCustomElementType(...)

This lets them replace a React component with a custom component that uses (or even doesn’t use) custom elements at any point in time.

So, if people are going to create React components at interop points, we might as well provide a powerful helper to do so. It is also likely that people will share those configs for custom elements they use.

And eventually, if we see the ecosystem stabilize, we can adopt a config-less approach.

I think the next step here would be to write a detailed proposal for how the config should look like to satisfy all common use cases. It should be compelling enough for custom element + React users, since if it doesn't answer common use cases (like event handling) we're going to end up in the limbo where the feature doesn't provide enough benefit to offset the verbosity.

Building from my earlier comment, how about:

const XFoo = ReactDOM.createCustomElementType('x-foo', {
  propName1: {
    propertyName: string | null,
    attributeName: string | null,
    attributeSerializer: function | null,
    eventName: string | null,
  }
  propName2: {
  }
  ...
});

The logic would then be, for each React prop on an XFoo instance:

  1. If the eventName for that prop is not null, register it as an event handler that invokes the prop value (assumed to be a function).
  2. Else if rendering client-side and propertyName is not null, set the element property to the prop value.
  3. Else if attributeName is not null, set the element attribute to the stringified prop value. If attributeSerializer is not null, use it to stringify the prop value. Otherwise, just do '' + propValue.

Polymer’s paper-input element has 37 properties, so it would produce a very large config.

I'd like to suggest that the config only be necessary for outlier props. For any prop on the XFoo instance that wasn't included in the config, default it to:

  • if the value is a function:
eventName: the prop name,
  • else:
propertyName: the prop name,
attributeName: camelCaseToDashCase(the prop name),

Alternatively, maybe it makes sense to keep events in a separate namespace, in which case, remove everything having to do with eventName from the last comment, and instead let events be registered as:

<XFoo prop1={propValue1} prop2={propValue2} events={event1: functionFoo, event2: functionBar}>
</XFoo>

@gaearon @effulgentsia what do y'all think of a combination of option 1 and option 5?

Option 1 would make it easier for the casual user of a custom element to pass rich data. I'm imagining the scenario where I'm building an app and I just want to use a couple of custom elements. I already know how they work and I'm not so invested that I want to write a config for them.

Option 5 would be for folks who want to use something like paper-input all over their app and would really like to expose its entire API to everyone on their team.

For SSR of option 1 the heuristic could be always use an attribute if rendering on the server. A camelCase property gets converted to a dash-case attribute. That seems to be a pretty common pattern across web component libraries.

I like the idea of an option1 + option5 combination a lot. Meaning that for most custom elements:

<x-foo prop1={propValue1}>

would work as expected: prop1 set as a property client-side and as a (dash-cased) attribute server-side.

And people could switch to option5 for anything for which the above doesn't suit them.

It would be a breaking change though from the way React 16 works. For anyone who experiences that breakage (e.g., they were using a custom element with attributes that aren't backed by properties), they could switch to option5, but it's still a break. I leave it to the React team to decide if that's acceptable.

Ah, this is what I get for reading this quickly on the train @robdodson đŸ€Šâ€â™‚ïž ... Not really a fan of option 3 now đŸ€” I read it as an all in on props being prefixed, hence my hesitation.

Option 5 seems reasonable and straightforward.

I like where @effulgentsia is heading. Is there a reason it couldn't be:

const XFoo = ReactDOM.createCustomElementType('x-foo', {
  propName1: T.Attribute,
  propName2: T.Event,
  propName3: T.Prop
})

Or is supporting multiple types on a single prop valuable?

I'd be hesitant with this flow though @effulgentsia:

if the value is a function:
eventName: the prop name,
else:
propertyName: the prop name,
attributeName: camelCaseToDashCase(the prop name),

I don't think I'd want a function prop to default to an event, and is assigning both propertyName and attributeName sensible? When would you want both supported to mimic the question above? 🙂

@LeeCheneler:

Quoting from the issue summary's Option 1 pros:

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.

So that's the reason why assigning both propertyName and attributeName is sensible: because it reflects what is actually the case for elements that follow best practice. And by making React aware of that, it allows React to decide which to use based on situation: such as using properties for client-side rendering and attributes for server-side rendering. For custom elements that don't follow best practice and have some attributes without corresponding properties and/or some properties without corresponding attributes, React would need to be aware of that, so that attribute-less-properties aren't rendered during SSR and property-less-attributes can be set with setAttribute() during client-side rendering.

With your proposal, that could potentially be done by bit-combining flags, such as:

propName1: T.Property | T.Attribute,

However, that wouldn't provide a way to express that the attribute name is different from the property name (e.g., camelCase to dash-case). Nor would it provide a way to express how to serialize a rich object to an attribute during SSR (the current behavior of "[object Object]" isn't useful).

I don't think I'd want a function prop to default to an event

Yeah, I think I agree with that as well, hence the follow-up comment. Thanks for validating my hesitance with that!

Here's a thought for a less verbose version of my earlier suggestion:

const XFoo = ReactDOM.createCustomElementType('x-foo', {
  UNREFLECTED_ATTRIBUTES: [
    'my-attr-1',
    'my-attr-2',
  ],
  UNREFLECTED_PROPERTIES: [
    'myProp1',
    'myProp2',
  ],
  REFLECTED_PROPERTIES: {
    // This is default casing conversion, so could be omitted.
    someVeryLongName1: 'some-very-long-name-1',

    // In case anyone is still using all lowercase without dashes.
    someVeryLongName2: 'someverylongname2',

    // When needing to define a function for serializing a property to an attribute.
    someRichData: ['some-rich-data', JSON.stringify],
  },
});

And per the code comment above, I strongly urge not requiring every reflected property to be defined, but rather default anything that isn't defined as automatically being a reflected property whose attribute name is the dash-cased version.

Makes sense @effulgentsia 👍

I like your second example but is it not open to combinatorial explosion if more types gets added, ala events + whatever might make sense?

- UNREFLECTED_ATTRIBUTES
- UNREFLECTED_PROPERTIES
- UNREFLECTED_EVENTS
- REFLECTED_PROPERTIES_ATTRIBUTES
- REFLECTED_PROPERTIES_EVENTS
- REFLECTED_ATTRIBUTES_EVENTS
- REFLECTED_PROPERTIES_ATTRIBUTES_EVENTS
...

Although I suppose you wouldn't want to mix an event with a prop or attribute anyway đŸ€” Attribute & prop are probably the only things you'd want to mimic.

I think there is an opportunity here for both the React and Web Component community to align on a best practice. React having an opinion here will go a long way in custom element authors being guided in the right direction due to its widespread adoption and weight that its opinions carry.

Although I've authored the implementation of option 4, I'm always caught up by having to separate attributes and events from properties. Ideally, I'd prefer option 1. Practically, I think I'd prefer option 2 with an escape hatch.

Option 1 is ideal but there are many types of attributes that don't have corresponding properties (aria / data), so it'd require extra heuristics around these and if there's any edge-cases where elements may have an attribute that should have a property, but do not implement one for whatever reason. I feel this is an option that should be considered carefully, but may be viable long-term.

Option 2 is preferable because it's a non-breaking change. It will work for all situations where the custom element definition is registered prior to an instance being created (or loaded before properties are set). Prior art for this option is Preact (cc @developit) and it's worked well thus far. Going down this path gives a reasonably robust, non-breaking implementation that has been proven by a successful React variant and it works in most situations. If anything, it gives React a short-term solution while better solutions are assessed for the long-term.

For the situation (situations?) where it doesn't work - deferred loading of custom element definitions and any others that we haven't covered - an escape hatch like what Incremental DOM has done, could be implemented. This is similar to what @effulgentsia is proposing, but would scale better to x number of custom elements. If consumers want to do it on a per-custom-element basis, they still can, because it's just a function. This allows React to have an opinion, escape hatch and satisfy all use-cases by handing off the responsibility to the consumer for all edge-cases. This is also something that I've previously discussed with @developit about implementing in Preact. Alignment between Preact / React here would be a big win.

About the concern with future HTML attributes, this is something that we can't solve, so I don't think we should get caught up in concerning ourselves with it here.

Those ReactDOM.defineCustomElementProp() calls could be provided in a JS file maintained by the custom element author

This would tie the custom element implementation to library-specific implementation (in this case React). In my opinion that is too high of a burden on custom elements authors. Moreover, for authors that do not use React, convincing them to ship the definition gets into politics discussions that no one wants.

Defining the invocation of such an API by the user of a custom element with only the properties/attributes the user actually uses is less code to maintain and more expressive.

Even if a library author does not use React, I am arguing it is a better UX for the client of those web components to define the property configuration once and then use it.

The first time you use a web component that needs a property, it is just as hard as adding a sigil (more verbose, but also more explicit) but then it is easier for subsequent uses because you don't need to think about it for every single callsite of that component. In either case you need to understand properties/attributes difference when adopting a new web component, but with the single shared configuration you don't need every user of that component within the same app to think about it.

We could potentially allow remapping property names in that configuration to allow making more React-idiomatic names there. The upgrade path to a more sophisticated wrapper, if one were ever needed, is also smoother since you can just replace XFoo with a custom React component that does whatever fancy translation logic it needs.

Basically: if it's possible for people to not think about the property configuration every time they use a component, I'd rather that approach. That's why I suggested option 5. I think it is more ergonomic than 3 and 4 while being equally flexible.

Option 1 and 2 don't work super well with server rendering (HTML generation), and option 2 also creates upgrade hazards for web component authors where now adding a property with the same name as an existing attribute is a breaking change. If we decide that SSR isn't useful then option 1 is pretty appealing from a React perspective. I'm not familiar if it would be comprehensive enough in practice though – do component authors expose everything via properties?

@treshugart without getting too deep into bikeshedding, do you think you could show how you'd expect the "escape hatch" to work? Pseudo code is fine. Just so we're all on the same page.

Sorry @sophiebits just now seeing your reply. I'd like to respond to a few of the points after reading through it a few more times but wanted to quickly respond to this one:

I'm not familiar if it would be comprehensive enough in practice though – do component authors expose everything via properties?

Any component built with Polymer or Skate will expose everything via properties. There might be a few rare outliers (maybe setting an attribute just for styling or something) but otherwise I think things are always backed by properties. I'm pretty confident that the majority of in-production web components are built using one of these two libraries.

If someone is hand-authoring a vanilla web component there is nothing that forces them to expose everything via properties but we encourage they do so in our best practices docs and examples.

For what it's worth, there's also nothing forcing them to use attributes. A custom element definition is just a class so the developer can do whatever they want. But in practice most folks prefer using an abstraction like Polymer or Skate to mint their components, so they're getting a properties API for free.

Maybe then the best approach is to do properties always on the client side and then support an Option 5–style configuration map of primitive property names to attribute names for people who want to support SSR in their apps.

@sophiebits: I think that's a great idea, except to the extent that it's a breaking change for people who've written their React apps using attribute names. For example:

<x-foo long-name={val} />

But what about a rule for "do properties" if the propname doesn't have dashes and "do attributes" if it does?

@robdodson: are you aware of any custom element frameworks or practices out there where people would have different casing of attributes from the corresponding property, without containing a dash? I.e., a longname attribute with a longName property? That actually seems to be the much more common pattern with built-in HTML elements and global attributes (e.g., contenteditable => contentEditable), but I'm unclear as to if it's now sufficiently discouraged for custom element attributes thanks to Polymer and Skate. If it's still an issue, then existing JSX with:

<x-foo longname={val} />

would fail as a property set if the property is longName.

@effulgentsia I can only speak for Skate, but we only recommend using the attribute API if writing HTML, which - for all intents and purposes - can also be classified as server rendering. If you're doing anything via JS, you should be setting props. Our props API automatically does a one-way sync / deserialisation from attributes, and derives the attribute name by dash-casing the property name (camelCase becomes camel-case, etc). This behaviour as a whole is configurable but we encourage the best practice to be the aforementioned.

As stated by many, props make SSR hard, and this is a valid concern. Since it sounds like most would prefer option 1, maybe we try and push forward with @sophiebits's proposal of setting props on the client while providing a fallback / mapping? I assume this means that attributes will be set on the server?

For the sake of an example, here's how you could implement carrying over state and props from the server to the client with Skate (and any custom element that implements a renderedCallback() and props and / or state setters. Doing so at the custom element level is trivial. If React went down the road of setting attributes on the server, rehydration would essentially be the same. Skate component authors actually wouldn't have to do anything as we'd already provide the deserialisation logic for them.

@robdodson I would do a similar thing to what Incremental DOM is doing. This would look something like:

const isBrowser = true; // this would actually do the detection
const oldAttributeHook = ReactDOM.setAttribute;

// This is much like the IDOM impl but with an arguably more clear name.
ReactDOM.setAttribute = (element, name, value) => {
  // This is essentially option 2, but with the added browser check
  // to keep attr sets on the server.
  if (isBrowser && name in element) {
    element[name] = value;
  } else {
    oldAttributeHook(element, name, value);
  }
};

@sophiebits

option 2 also creates upgrade hazards for web component authors where now adding a property with the same name as an existing attribute is a breaking change.

I think this may be unavoidable given the way attributes work in the platform. If you SSR a custom element, and therefore must write to an attribute, you also run the risk that the platform will ship a similarly named attribute in the future. As @treshugart mentioned before, I don't think it's something React (or really any library) is empowered to solve. This is something I want to take up with the web component spec authors at TPAC to see if we can fix it in the custom element space.

I'm not sure if that changes your mind at all about option 2 😁 but I wanted to mention it since option 2 has a few nice bonuses and Preact seems to be proving it out in practice.

Maybe then the best approach is to do properties always on the client side and then support an Option 5–style configuration map of primitive property names to attribute names for people who want to support SSR in their apps.

+1, I'm supportive of continuing to head in this direction.


@effulgentsia

I think that's a great idea, except to the extent that it's a breaking change for people who've written their React apps using attribute names.

Option 2 would solve that :)
Otherwise there would probably need to be a heuristic coupled with option 1 that maps dash-cased attributes to camelCased properties.

But what about a rule for "do properties" if the propname doesn't have dashes and "do attributes" if it does?

It looks like Preact will set the attribute if there's a dash in the name. I'm assuming that's because they use option 2 and long-name doesn't pass the in check so it falls back to an attribute (source).

I personally like this behavior, but it gets back into the realm of setting attribute and possible future conflicts so I think @sophiebits should weigh in.

are you aware of any custom element frameworks or practices out there where people would have different casing of attributes from the corresponding property, without containing a dash?

Not that I know of. The dash is an easy way for the library to know where to camel case. If you were hand-authoring a vanilla web component you could do longname/longName and only option 2 would save you because it wouldn't find a longname property, but it'd fallback to the previously used longname attribute.

For what it's worth, it looks like Preact, as a last resort, will call toLowerCase() on a property it doesn't recognize before setting the attribute. So if you were SSR'ing <x-foo longName={bar}/> it would also properly fall back to the longname="" attribute.


@treshugart

maybe we try and push forward with @sophiebits's proposal of setting props on the client while providing a fallback / mapping? I assume this means that attributes will be set on the server?

yes, and yes.

I would do a similar thing to what Incremental DOM is doing

Is the idea that every element (or their base class) would need to do mix this in?

btw, thank you all for continuing to participate in the discussion, I know it's gotten quite long ❀

Not sure I understand the last example in https://github.com/facebook/react/issues/11347#issuecomment-339858314 but it’s highly unlikely we’d provide a global overridable hook like this.

@gaearon I think Trey was just showing the net change as a monkey-patch, presumably it'd be written into the ReactDOM implementation itself.

Based on the recent comments, what do you all think of this proposal?

  1. For BC, don't change anything about how lowercase custom elements work in React. In other words, JSX like <x-foo ... /> would continue to work the same way in React 17 as in 16. Or, if _minor_ bug fixes are wanted for it, open a separate issue to discuss those.

  2. Add a config-less API to create a React component with better custom element semantics. E.g., const XFoo = ReactDOM.customElement('x-foo');.

  3. For the component created above, employ the following semantics:

  4. For any prop on XFoo that is a reserved React prop name (children, ref, any others?), apply the usual React semantics to it (do not set it as either an attribute or property on the custom element DOM node).
  5. For any HTMLElement global attribute or property defined by the HTML spec (including data-* and aria-*), do the same thing with those props as what React would do with them if they were on a div element. In other words, how React applies the props on <XFoo data-x={} className={} contentEditable={} /> to the DOM node should be identical to how it does so for <div data-x={} className={} contentEditable={} /> (both for client-side and for SSR).
  6. For any prop on XFoo that contains a dash (other than the global attributes permitted above), emit a warning (to inform the developer that XFoo is a property-centric, not an attribute-centric, API) and do nothing with it (neither set it as a property nor an attribute).
  7. For any prop on XFoo that starts with an underscore or an upper ASCII letter, do nothing with it (and maybe emit a warning?). Neither is a recommended way to name properties for custom elements, so reserve those namespaces for future semantics (for example, see #4 below).
  8. For all other props on XFoo, when rendering client-side, set it on the DOM node as a property. For SSR, if the value is primitive, render it as an attribute; if non-primitive, skip rendering and set it as a property client-side during hydration. For SSR rendering as an attribute, camelCaseToDashCase the name.

  9. For components created via the API in #2 above, reserve a prop name to use for event listeners. E.g., 'events' or 'eventListeners'. Or, if not wanting to conflict with those potential custom element property names, then '_eventListeners' or 'EventListeners'. The ReactDom-created implementation of XFoo would automatically register these event listeners on the DOM node.

  10. For edge cases (e.g., using a custom element for which the above implementation isn't desired or sufficient), the app developer could implement their own React component to do whatever special thing they need. I.e., they don't need to use ReactDOM.customElement() or they could extend it.

  11. For people who want all of the above behavior, but want their JSX to use the lowercase custom element names (<x-foo /> instead of <XFoo />, for similar reasons that people familiar with writing HTML prefer <div> over <Div>), they can monkey-patch React.createElement(). It would be a pretty simple monkey patch: just take the first argument and if it matches a custom element name you want this for (whether that's a specific list or any string with all lowercase letters and at least one dash), then invoke ReactDOM.customElement() on that name and forward the result and remaining arguments to the original React.createElement().

@developit @gaearon it could be either. If a "mapping" was necessary, I think a hook is more sustainable. However, that was also intended to show the net change if it were to be implemented in ReactDOM's core, as Jason pointed out.

For BC, don't change anything about how lowercase custom elements work in React. In other words, JSX like would continue to work the same way in React 17 as in 16. Or, if minor bug fixes are wanted for it, open a separate issue to discuss those.

Personally I'd prefer to be able to use my <x-foo> element, as is, and easily pass it properties without first needing to wrap it.

If possible I'd rather go with @sohpiebits' suggestion of option 1 (client-side properties) and 5 (config for SSR). I'm still holding out hope that maybe folks will reconsider option2 (maybe option 2 + 5?) for the backwards compatability bonus.

@gaearon does your opinion change on Trey's proposal if it's part of ReactDOM core? Maybe we could flesh out the example more if that would help?

My main concern with Option 5 is creating a lot of boilerplate to allow interoperability, breaking the DX, it would be a shame for all the React core team and contributors to spend a lot of time in changes that won't have the desired impact.

I really loved how the React team handled the last changes in the repo, like _PropTypes_, it would be good to think of a plan involving more than just one switch, to educate developers in possible changes to do in the future for custom and not custom elements.

I think that the solution that will satisfy all of us will be one combining some of this options as _steps_, API addition, warning and deprecation or behavior change.

Maybe options 5, with warning, later option 2 with deprecation of the needed wrapper.

Maybe options 5, with warning, later option 2 with deprecation of the needed wrapper.

I was actually thinking that doing the opposite would be a better series of steps. Option 1 or 2 because it's less of a dramatic change. And measuring how that goes and what the SSR story starts to look like for web components. Then following up with option 5 because it adds a new API and a notion of proxy/wrapper components.

The main problem with option 1 is that it's a pretty large BC break. The main problem with option 2 is that it has a race condition depending on when the element is done upgrading. I'm not sure that either of those is truly acceptable for a project that's as widely used as React is.

Given the availability of option 5, I wonder if we can instead make much safer, but still useful, improvements to non-option-5 usages. For example, how about the inverse of option 4: simply introduce domProperties and eventListeners props, so you can do stuff like:

<x-foo 
  my-attr1={...} 
  domProperties={{myRichDataProperty: ...}} 
  eventListeners={{'a-custom-element-event':  e => console.log('yo')}} 
/>

This is fully backwards compatible, because by virtue of their uppercase letters, domProperties and eventListeners are not valid attribute names. Except for that React 16 currently calls setAttribute() even for prop names with upper alphas, relying on the browsers to internally lowercase the name; however, could a future minor release of React 16 emit a warning when encountering custom element props that are not valid attribute names, so that people can fix their casing mistakes prior to upgrading to React 17?

In addition to preserving BC, this approach is easy to understand: it's attribute-centric (meaning React props are treated as element attributes), which fits given that the element name itself is all lowercase and has a dash and is therefore HTML-centric. However, domProperties is provided for the relatively rare cases where you need to pass properties, such as to pass rich data.

For people who want to switch their mental model to be property-centric (ala Option 1), that's where an option 5 style API could come in:

const XFoo = ReactDOM.customElement('x-foo');
<XFoo prop1={} prop2={} data-foo={} aria-label={} />

With this syntax, every prop is treated as a property (option 1). Which fits, because the element "name" (XFoo) is also following a JS-centric convention. I think we'd still want to at a minimum support data-* and aria-* props treated as attributes, which we could either limit to just those, or generalize to treating any prop with a dash as an attribute.

Meanwhile, to support option 5 configuration of SSR, we could add a config API to ReactDOM.customElement, such as:

const XFoo = ReactDOM.customElement('x-foo', ssrConfiguration);

Perhaps ssrConfiguration could be a callback function similar to the ReactDOM.setAttribute one in @treshugart's comment?

What do you think?

@effulgentsia I like where your thoughts are going. Bikeshedding the names a bit: domProps / domEvents. This is very close to option 4.

WRT SSR, I think SSR can be handled by the custom elements themselves so long as React could respect attributes the component mutates onto itself if it's not tracking them. I posted a gist earlier, but here it is again, for convenience: https://gist.github.com/treshugart/6eff0da3c0bea886bb56589f743b78a6. Essentially the component applies attributes after rendering on the server and rehydrates them on the client. SSR for web components is possible, but solutions are still being discussed at the standards level, so it may be best to wait on this part of the proposal.

@effulgentsia I also like where you're heading. @sophiebits, @gaearon do
y'all have thoughts on this direction?

On Tue, Oct 31, 2017, 7:33 PM Trey Shugart notifications@github.com wrote:

@effulgentsia https://github.com/effulgentsia I like where your
thoughts are going. Bikeshedding the names a bit, it might be useful to
align their naming: domProps / domEvents, or something.

WRT option 2, it's at least backward compatible and solves most use-cases,
but I'm coming around to the alternatives.

I think SSR can be handled by the custom elements themselves so long as
React could respect attributes the component mutates onto itself if it's
not tracking them. I posted a gist earlier, but here it is again, for
convenience:
https://gist.github.com/treshugart/6eff0da3c0bea886bb56589f743b78a6.
Essentially the component applies attributes after rendering on the server
and rehydrates them on the client. SSR for web components is possible, but
solutions are still being discussed at the standards level, so it may be
best to wait on this part of the proposal here.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/facebook/react/issues/11347#issuecomment-340960798,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABBFDeiQhBWNGXNplbVV1zluYxT-ntFvks5sx9hngaJpZM4QD3Zn
.

Bikeshedding the names a bit: domProps / domEvents.

I like those. I'm also brainstorming on if there's a way to make them even more idiomatic to React by replacing the concept of "dom" with the concept of "ref". So in other words: refProps / refEvents, since they're about attaching props and event listeners to the "ref".

And then I thought, what if instead of introducing new special names inside of this.props, we simply overload the existing ref JSX attribute. Currently, "ref" is a function that is called when the React component is mounted and unmounted. What if we allowed it to also be an object as so:

<x-foo my-attr-1={}
  ref={{
    props: ...
    events: ...
    mounted: ...
    unmounted: ...
  }}
/>

Just an idea.

I really like this approach :)

Friendly ping on this. @gaearon @sophiebits do y'all have any thoughts on @effulgentsia's latest proposal? Just curious if it's in the ballpark or a non-starter.

We just opened an RFC process. Could we ask either of you to submit it as an RFC?
https://github.com/reactjs/rfcs

I would rather not add domProperties and eventListeners "props" (or the equivalent within a ref={{}} object) because it makes using custom elements very unnatural and unlike all other React components. If the component user is going to need to acutely know the difference between properties and attributes, etc, then I would rather do it as a ReactDOM.createCustomElementType-style solution. Then if you use the component exactly once it is a comparable amount of work (specifying the configuration and then using it once), but if you use the component many times then you don't need to think about the configuration object every time. Requiring that the configuration be specified every time seems to defeat the goals of having a clean custom elements integration unless I'm missing something.

@sophiebits OK, I could attempt to write up an RFC for something like that. What do you think about the idea you floated back on October 26 of going properties first on the client side and also allowing folks to write ReactDOM.createCustomElementType for SSR and/or if they want really fine grained control over how the client maps attrs/properties?

At least with the createCustomElementType style, libraries can easily map their APIs into that. I.e., our skatejs/renderer-react could easily take the props and configure the custom element for React. This sort of leaves vanilla folks high and dry without an abstraction or performing a bit of work, though. I like Rob's suggestion of a safe default, while allowing fine-grained control. Is that something that would work?

@robdodson I think I'm on board with it. Can't promise no other concerns will come up but I think it feels like a good balance.

Option 3 is the best option so far, and it can work with SSR, I'll explain an idea of how.

So far all options by themselves, except for 3,

  • either create high maintenance burden for everyone outside of React source
  • or they force all custom element authors in the entire universe to abide by React-specific rules.

Here's universal rules we all agree on:

  • setting attributes with setAttribute is the most standard way in the universe for passing data to elements in a way that matches 1-to-1 with Declarative HTML attributes. This has to work 100% of the time as a law of the universe, therefore it's the only 100% guaranteed way to pass data to elements.
  • some people aren't happy that it was designed only for strings.
  • Some elements (I repeat, _only some elements_), accept values via object properties that map to certain attributes. This is _not_ something that can be relied on 100% of the time.
  • some people like object properties because they can accept non-strings

So, at bare minimum, if React wants to work 100% with _every single custom element in the universe and not be a burden for people_, then:

  • React needs to realize it isn't God, there's other many other libraries that people use besides react,
  • therefore React shoud _by default_ just pass data via setAttribute because that is 100% standard.
  • React should accept the fact that Custom Element authors can extend/override the setAttribute methods in their class definitions, making setAttribute accept things other than strings. A prime example can be libraries like A-frame.
  • React should accept that if a custom element author wants a custom element to work with every possible library, _not just with React_ then that author will rely on setAttribute to make his element by default compatible with everything, and if by default all libraries rely on attributes, then the whole universe will work with each other. _There's no ifs, ands, or buts about this!_ (unless the w3c/whatwg makes some big changes)

Soooooooo, that being said, we should at the very least

Then, we can think about the implications of elements sometimes having object-property API:

  • developers have the choice to use setAttribute knowing it will work all the time.
  • developers can sometimes take advantage of object properties.
  • developers always have to be aware whether or not an object-property interface exists for any given element (custom or not).
  • object-property interfaces are an alternative interface that don't necessarily map 1-to-1 with attributes.

So, with this knowledge of attributes vs properties, a solution in React that _wishes to augment the 100% standard and respect laws of the universe_ should:

  1. allow attributes to work 100% of the time by default. This means that <x-foo blah="blah" /> should _by default_ map to setAttribute and pass the value along _unchanged_. This is a non-breaking change. In fact, it is a fixing change that would otherwise result in meaningless "[object Object]" string being passed in.
  2. Come up with an alternative way to let the React user _optionally_ use props if the user is conscious about which object-property interfaces exist and wants explicitly use those interfaces.

It seems like Option 3, using a sigil (who's extra syntax is honestly not hard to learn), is a solution that gets closest to ideal. Based on this SO question, then the only available symbol is =, though settling on something like & (with an escapable form, perhaps like \&) is more readable. For example, if I want a prop specifically:

<x-foo &blah="blah" />

Most other characters covered by the WHATWG HTML syntax spec should work, and I hope that they do but that's another topic.

Option 3 is the best option so far. How can SSR work? Just serialize the prop data (with known limitations), then set the object properties on the client side during hydration.

If hydration isn't being used on the client and therefore props don't make sense in SSR, then, oh well. It never worked before, and it doesn't need to work now. PHP-style or Java-style SSR sends static HTML with no hydration and they rely 100% on attributes. It goes to say, if we use React SSR, we will probably use client-side hydration, and if we don't want hydration, then we should simply be aware of the fact that we shouldn't use props in this case. _This is simple. This is how the web works. All that react has to do is make this caveat clear in the documentation._

But!!! That's not all! We can still include the features of Option 5 to give people more control. With an API like Option 5,

  • Some people can configure how some attributes can map to props
  • AND how some props can map to attribues. This can help people make SSR work even for the non-hydrating type of SSR!
  • we can let people define "if this attribute is written, actually pass it to this prop, but if it is SSR, don't pass it to the prop", or "pass this prop to this attribute only during SSR, otherwise do what I specified using the sigil", etc

In the end, the following seems like a solution that would work:

  • Option 3 with setAttribute used by default behind the scenes,
  • with a fix for #10070 so that React doesn't get in people's way,
  • and an API like in Option 5 for further tuning for those that really need it, including ways to tune SSR, client, or both.

Happy new year!

I want to respond to some of the above points, but fear that this thread is already _incredibly_ long. So, sorry for making it longer :P

Here's universal rules we all agree on:

setting attributes with setAttribute is the most standard way in the universe for passing data to elements in a way that matches 1-to-1 with Declarative HTML attributes. This has to work 100% of the time as a law of the universe, therefore it's the only 100% guaranteed way to pass data to elements.

This isn't entirely true. There is nothing in the platform to enforce that a custom element expose an attributes interface. You could just as easily create one that only accepts JS properties. So it's not a "guaranteed way to pass data". The lack of enforcement mechanisms means that you can't rely on either style (HTML attributes or JS properties) with 100% certainty.

some people aren't happy that it was designed only for strings.
Some elements (I repeat, only some elements), accept values via object properties that map to certain attributes. This is not something that can be relied on 100% of the time.

We encourage folks to not even bother creating attributes for properties which accept rich data like objects/arrays. This is because some data cannot be serialized back to an attribute string. For example, if one of your object properties is a reference to a DOM Node, that can't actually be stringified. Also, when you stringify and reparse an object, it loses its identity. Meaning, if it has a reference to another POJO, you can't actually use that reference since you've created an entirely new object.

some people like object properties because they can accept non-strings

therefore React shoud by default just pass data via setAttribute because that is 100% standard.

JavaScript properties are equally as standard. Most HTML elements expose both an attributes and corresponding properties interface. For example, <img src=""> or HTMLImageElement.src.

React should accept the fact that Custom Element authors can extend/override the setAttribute methods in their class definitions, making setAttribute accept things other than strings.

Authors could do that, but that actually seems way more "non-standard" than just using JS properties. It may also expose you to weird issues related to parsing and cloning the element.

React should accept that if a custom element author wants a custom element to work with every possible library, not just with React then that author will rely on setAttribute to make his element by default compatible with everything, and if by default all libraries rely on attributes, then the whole universe will work with each other. There's no ifs, ands, or buts about this! (unless the w3c/whatwg makes some big changes)

I'm not sure how you arrive at this conclusion because there are other libraries which prefer setting JS properties on custom elements (Angular, for example). For maximum compatibility, authors should back their attributes with JS properties. That will cover the largest surface area of possible uses. All elements created with Polymer do this by default.

allow attributes to work 100% of the time by default. This means that should by default map to setAttribute and pass the value along unchanged. This is a non-breaking change. In fact, it is a fixing change that would otherwise result in meaningless "[object Object]" string being passed in.

I think React actually _is_ passing the value unchanged. Calling setAttribute('foo', {some: object}) results in [object Object]. Unless you're proposing that they call JSON.stringify() on the object? But then that object isn't "unchanged." I think maybe you're relying on the author to have overridden setAttribute()? It may be more plausible to encourage them to create corresponding JS properties instead of monkey-patching the DOM.

I think React actually _is_ passing the value unchanged. Calling setAttribute('foo', {some: object}) results in [object Object]

React is coercing values to a string before passing into setAttribute:

https://github.com/facebook/react/blob/4d6540893809cbecb5d7490a77ec7ad32e2aeeb3/packages/react-dom/src/client/DOMPropertyOperations.js#L136

and

https://github.com/facebook/react/blob/4d6540893809cbecb5d7490a77ec7ad32e2aeeb3/packages/react-dom/src/client/DOMPropertyOperations.js#L166

I basically agree with all you said.

We agree that people are doing things both ways, and there's not a standard that forces everyone to do it just one way or the other, so I still think

  • Option 3 with setAttribute used by default and a sigil for specifying to use instance properties,
  • with a fix for #10070 so that React doesn't coerce args to strings,
  • and Option 5, an API for fine tuning

If Option 5 is done well, then hydration for SSR solutions can map data to either attributes or props as can be specified by the use of Option 5 API.

React is coercing values to a string before passing into setAttribute

I see. Since most folks don't define a custom toString() it defaults to [object Object].

Since most folks don't define a custom toString() it defaults to [object Object].

Just like if I do

const div = document.createElement('div')
div.setAttribute('foo', {a:1, b:2, c:3})

the result is

<div foo="[object Object]"></div>

Obviously as a web developers we should be aware what happens when we pass a non-strings into element attributes. For example I'm aware that I can pass non-strings into A-Frame elements, and I should be free to do that without a library getting in the way.

React needs to realize it isn't God, there's other many other libraries that people use besides react

This is unnecessarily snarky. You'll note in this thread that we do care about this but that there are many different options and visions for where to take the custom element design. It's certainly not obvious what should be done.

@sebmarkbage Sorry about that, I didn't mean to be snarky at all, and I think React is a nice lib. I should've thought about my words more carefully there (especially because not everyone has the same religion).

What I meant is, React is very popular, so React has the potential to sway people to do things in a certain way that may not work in other places (f.e. it could tell people to rely on instance properties for all Custom Elements which wouldn't work with all Custom Elements).

React currently converts all values passed to element attributes into strings. Had React not done this, for example, there would be no need for aframe-react (which works around the string problem) to even exist.

If React can just let us have the ability to make any choice about how we pass data to elements, just like in plain JavaScript, that'd make me the most satisfied user. 😊

Again, sorry about my choice of words there. I'll double think it next time.

I've added a comment to the RFC PR for this. I think it's worth discussing as it covers what's being proposed as well as a simpler model for reliably inferring a custom element and its properties. It turns it back into a hybrid approach, but offers a zero-config way of integrating for most use cases.

I'm keen to see this feature implemented in React. Thanks a lot for your efforts making React great.

Any update on this RFC?

Custom Element libraries are getting really good and I would love to use them in my React app. Any news on this yet? Wrapping custom elements and stringifying their contents to later parse them again is a pretty unworkable solution considering Vue and Angular are handling components natively with ease

Any update on this issue?

I too, would love to use custom element libraries without resorting to hacks. I'd love for this issue to be resolved.

@mgolub2 React team doesn't care about what the web community wants. Web Components are now a widely supported standard, and there aren't any sort of signals from the team to support this standard, after 2 years.

Hey everyone, I started a pull request to fix this issue by changing two lines: https://github.com/facebook/react/pull/16899

This allows a custom element author to do something like the following:

class MyElement extends HTMLElement {
  setAttribute(name, value) {
    // default to existing behavior with strings
    if (typeof value === 'string')
      return super.setAttribute(name, value)

    // but now a custom element author can decide what to do with non-string values.
    if (value instanceof SomeCoolObject) { /*...*/ }
  }
}

There's many variations on what an extended setAttribute method could look like, that's just one small example.

React team, you may argue that custom element authors shouldn't do that, because they bypass the DOM's native attribute handling in some cases (f.e. when values are not strings). If you do have that opinion, that still does not mean you should impede on what custom element authors can do with their __custom__ elements.

React should not be making opinions on how existing DOM APIs are used. React is a tool for manipulating DOM, and should not be opinionated on what we can do to the DOM, only in what way data flows to the DOM. By "way data flows to the DOM" I mean the route the data takes to get there, without mutation to the data (converting an author's objects to strings is mutating the author's data).

Why do you want to mutate the author's data? Why can not you just assume the person who wishes to manipulate the DOM knows what data to pass to the DOM?

@trusktr I think this was discussed in https://github.com/facebook/react/issues/10070

I'd really caution folks against telling custom element authors to override a built-in method like setAttribute. I don't think it's intended for us to monkey patch it. cc @gaearon

It is harmless to override setAttribute in subclasses like that (that's not monkey patching). But as I mentioned, why does React have to dictate on that, if that's not necessarily the job of the React lib. We want to use React to manipulate DOM (without hinderance).

If I have to use el.setAttribute() manually for a performance boost, that also just makes the dev experience worse.

I don't think the React team is saving many people from some huge peril by converting anything passed into setAttribute to strings.

I do agree another solution may be better. For example, updating the JSX spec to have some new syntax, but that seems to take long.

What does the community lose if we take away the automatic string conversion? What does the React team lose?

The React team could improve the situation later, with a better solution...

Why not at least simply give us an option to bypass the stringification? Is that something you might be willing to consider?

I'd really caution folks against telling custom element authors to override a built-in method like setAttribute.

Can you provide a good reason as to why?

This seems like a red herring. "Convince everyone writing custom elements to attach a method with specific behavior to their element" is not a solution to this issue, which is about how to set properties on DOM elements.

@trusktr

Can you provide a good reason as to why?

Web components are a standard. So, derivatives of the standard should be standard-compliant as well.

However, overriding setAttribute does not fit this condition: it creates a hack only for React while there is a lot of other frameworks that work with web components out of the box. So, they would need to consume the React hack even when they don't work with React at all. I don't think it is the right solution.

Next, it is well-known that patching the standard methods is a wrong approach. Overriding setAttribute changes the original behavior that can make end users confused when they try to use it. Everyone expects standard to work as standard, and the custom element is no exception because it inherits the HTMLElement behavior. And while it might work with React, it creates a trap for all other users. E.g., when web components are used without a framework, setAttribute may be called a lot. I doubt the custom element developers would agree to shoot their foot with this approach.

Personally, I think that some kind of React wrapper looks way more promising.

Hey folks, in the meantime while we wait, I created a shim for wrapping your web component in React https://www.npmjs.com/package/reactify-wc

import React from "react";
import reactifyWc from "reactify-wc";

// Import your web component. This one defines a tag called 'vaadin-button'
import "@vaadin/vaadin-button";

const onClick = () => console.log('hello world');

const VaadinButton = reactifyWc("vaadin-button");

export const MyReactComponent = () => (
  <>
    <h1>Hello world</h1>
    <VaadinButton onClick={onClick}>
      Click me!
    </VaadinButton>
  </>
)

I hope this proves helpful

(This is my first foray into OSS, and one of the first open-sourcing of something out of my office. constructive criticism is more than welcome 😄 )

Hey folks, in the meantime while we wait, I created a shim for wrapping your web component in React https://www.npmjs.com/package/reactify-wc

import React from "react";
import reactifyWc from "reactify-wc";

// Import your web component. This one defines a tag called 'vaadin-button'
import "@vaadin/vaadin-button";

const onClick = () => console.log('hello world');

const VaadinButton = reactifyWc("vaadin-button");

export const MyReactComponent = () => (
  <>
    <h1>Hello world</h1>
    <VaadinButton onClick={onClick}>
      Click me!
    </VaadinButton>
  </>
)

I hope this proves helpful

(This is my first foray into OSS, and one of the first open-sourcing of something out of my office. constructive criticism is more than welcome 😄 )

Great wrapper :)

It's also worth mentioning that building web components with Stencil fixes this issue with React: https://stenciljs.com/docs/faq#can-data-be-passed-to-web-components-

@matsgm I'm glad building with Stencil has worked for you. However, as a word of caution, in our experience Stencil has proven to not play nicely with other web component frameworks, specifically Polymer, and we've been annoyed with a half dozen other issues between their build tools, support, and general functionality. Your Mileage May Vary :)

Just trying to get up-to date with this thread. What is the final solution ?

In a way I am a big fan of explicit defining attr, prop, event rather than magic heuristics which can be very confusing.

E.g snabbdom uses explicit top level namespacing, this makes it easy to know what goes where. There is no string manipulation which is faster to execute e.g trimming suffix from onClick => click is still a perf hit.

<input attrs={{placeholder: `heyo`}} style={{color: `inherit`}} class={{hello: true, world: false}} on={{click: this.handleClick}} props={{value: `blah`}} />
attr = (attr, val) => elem.setAttribute(attr, val);
prop = (prop, val) => elem[prop] = val;
on  = (event, handler) => elem.addEventListener(event, handler)
style = (prop, val) => elem.style[prop] = val;
class = (name, isSet) => isSet ? elem.classList.add(name) : elem.classList.remove(val)
dataset = (key, val) => elem.dataset[key] = val;

I wish JSX supported namespacing with dot syntax. This means props would be default namespace and we could simply write

<div tabIndex={-1} attr.title={"abcd"} on.click={handler} style.opacity={1} class.world={true} />`

fyi @sebmarkbage ^

const whatever = 'Whatever';
const obj = { a: 1, b: 2 };
const reactComponent = (props) => (
<div>
  ...
  <custom-element attr="{whatever}" someProp={obj} />
  { /* double quotes for attributes */ }
  { /* no quotes for properties */ }
</div>
);

This is achievable by modifying the React JSX Pragma parser.

Additional option is to keep propeties (or props for the react fans) as a designated word for property passing

Since 17.0 got released today, could we update this issue to reflect the status of when this will be addressed?

Yeah. We originally planned React 17 to be a release with a lot more deprecations removed (and with new features). However, it's not very realistic for old codebases to update all of their code, and this is why we've decided to get out React 17 that only focuses on a single significant breaking change (events are attached to the root element rather than the document). This is so that old apps can stay on React 17 forever and only update parts of them to 18+.

I know it's frustrating we've been saying this particular feature would go into 17, but you'll notice pretty much all of the things we originally planned for 17 were moved to 18 too. 17 is a special stepping stone release. Which lets us making more aggressive breaking changes in 18. Potentially including this one if there is a clear path forward.

I'm not sure what the latest consensus is from the WC community on which of these options is preferable. It's very helpful to have all of them written down though (big props to @robdodson for doing that work). I'm curious if people's opinions on these options have evolved since this thread was written, and if there is any new information that could help us pick the direction.

I don't think the WC community has changed their preferred option, which is still option 3. @developit can speak more about how Preact is compatible with custom elements, which might be interesting for React as well. For a general overview of how frameworks are compatible with passing (complex) data into custom elements, https://custom-elements-everywhere.com/ has all the details.

Note that in https://github.com/vuejs/vue/issues/7582, Vue chose to use a sigil, and they chose "." as a prefix (not Glimmer's "@").

In https://github.com/vuejs/vue/issues/7582#issuecomment-362943450, @trusktr suggested that the most correct SSR implementation would be to not render sigil'd properties as attributes in the SSR'd HTML, and to instead set them as properties via JS during hydration.

I think it's pretty unlikely that we would introduce new JSX syntax for the sake of this particular feature alone.

There's also a question of whether it's worth to do option (3) if a more generic version of the option (5) is possibly on the table. I.e. option (5) could be a low-level mechanism to declare custom low-level React nodes with custom mount/update/unmount/hydration behavior. Not even specific to Custom Elements per se (although they would be one of the use cases).

Rather than introducing a wholly new JSX syntax for this specific feature, what about introducing a more general JSX and using it to define custom properties?

I proposed adding ECMAScript's computed property syntax to JSX a long time ago (facebook/jsx#108) and think it would be a useful addition to the syntax in general.

If computed property syntax was available that would leave open the possibility of defining properties using computed property syntax and Symbols or prefixed strings.

For instance:

import {property} from 'react';
// ...
<custom-img [property('src')]="corgi.jpg" [property('hiResSrc')]="[email protected]" width="100%">

@dantman How does this proposal handle server rendering and hydration?

I don't think anyone in the WC community wants option 5.

It's really no different than the current practice of patching writing a custom React wrapper for custom elements. The massive downside of that approach is it puts a burden on either the component author to special-case React or the component consumer to special case custom elements, neither of which should be necessary.

What is your thinking on server rendering and hydration? If there is no explicit config, which heuristic is desirable? Has there been a consolidation in how this is usually done in the WC community? I re-read this RFC and it doesn't seem to dive into details about this topic. But it's pretty crucial, especially as React moves towards making more heavy emphasis on server rendering (as it's often rightfully criticised for too client-centric defaults).

@gaearon I don't know enough hydration and custom properties to know what is required, this is primarily a syntax proposal.

The general idea is that property(propertyName) could, depending on how you wish to implement it, output a prefixed string (e.g. '__REACT_INTERNAL_PROP__$' + propertyName) or create a Symbol and save a "symbol => propertyName" association in a map.

The later could be problematic if you need to communicate that map to the client.

However to my rough understanding properties aren't something you can handle on the server and hydration involves client code, so I'm not sure what the plan for that is. As for my proposal, it can probably be adapted to whatever plans you have to solve that issue. If there's something you plan to have react do with properties on the server then it can just do that when it sees one of the properties created by property.

As a web component library author, option 5 isn’t a convenient choice. I want to create custom elements without having to explicitly define a schema for each framework and component. And many custom element authors just won’t do this, pushing the burden on the React developer.

Nobody wins with option 5. 😕

@claviska Do you have any opinions about how server rendering and hydration should work with more implicit approaches?

@gaearon SSR will subdivide into two situations: those where the custom element supports SSR and those where it doesn't.

For elements that don't support SSR, setting properties on the server doesn't matter. Except for built-in reflecting properties like id and className, they can be dropped and only written on the client.

For elements that do support SSR properties will matter, but only so that they can trigger whatever result they cause on DOM. This requires an element instance or some kind of SSR proxy/stand-in, and some protocol for communicating the SSR'ed DOM state. There is no common server-side interface for this process yet, so there's really nothing to be done here for now. Web component authors and library maintainers will need to figure some things out before it'd be viable for React to build any bridges there.

In my team's work on SSR we have integrated with React by patching createElement and using dangerouslySetInnerHTML. I think that's the level of integration/experimentation we'll be at for a bit. At some point soon I hope we can converge on some user-land protocols for interop within SSR systems. Until then it's perfectly safe and prudent to have custom element property and event support without _deep_ SSR. The element's tag would still be SSR'ed as a child of a React component as it is today.

Except for built-in reflecting properties like id and className, they can be dropped and only written on the client.

Can you help me understand how this works? E.g. say there's <github-icon iconname="smiley" />. Do you mean the SSR should just include <github-icon /> in the HTML response, and then React will set domNode.iconname = ... during hydration? In that case, I'm not sure I understand what happens if the custom element implementation loads before the React hydration occurs. How will github-icon implementation know which icon to render if iconname does not exist in the HTML?

There is no common server-side interface for this process yet, so there's really nothing to be done here for now. Web component authors and library maintainers will need to figure some things out before it'd be viable for React to build any bridges there.

I'm curious if there's anything in particular that should happen for the community to form a consensus here. Is React the one holding it back? I very much understand the frustration associated with this issue being open since 2017. On the other hand, it's been three years, and I think you're saying this consensus has not formed yet. What are the prerequisites for it happening? Is it just a matter of more experimentation?

@gaearon if the custom element implementation load before the React hydration occurs, it will assign whatever the dafault value of iconname attribute is. What do you mean by if _iconname_ does not exist in the HTML? If HTMLElement type does not have an attribute defined, it will ignore it. once the custom element definition loads, it will extend the HTMLElement type and define iconname and be able to react to a new value being passed in.

I'm trying to understand this from a user's perspective. We're rendering a page on the server. It has an icon in the middle of a text. That icon is implemented as a custom element. What is the sequence of things that the end user should experience?

My understanding so far is that:

  1. They see the initial render result. It will include all the normal markup, but they will not see the <github-icon> custom element at all. Presumably it would be a hole, like an empty div. Please correct me if I'm wrong (?). Its implementation has not loaded yet.

  2. The <github-icon> custom element implementation loads and it gets registered. If I understand correctly, this is the process called "upgrading". However, even though its JS is ready, it still can't show anything because we have not included the iconname in the HTML. So we don't have the information for which icon to render. This is because we said earlier that "non-builtin properties can be dropped from the HTML". So the user still sees a "hole".

  3. React and the application code loads. Hydration happens. During the hydration, React sets the .iconname = property as per the heuristic. The user can see the icon now.

Is this a correct breakdown for the case where the custom element JS implementation loads first? Is this the behavior desirable for the WC community?

@gaearon yep that's what i would expect to happen in this situation.

On the other hand, it's been three years, and I think you're saying this consensus has not formed yet

I'm not saying consensus hasn't been met. I'm saying you don't need to worry about _deep_ SSR right now, and shouldn't let pretty vague concerns about it block the most basic client-side interoperability that can be accomplished and be extremely useful right now.

I haven't seen this distinction before (deep vs shallow) so let me try to restate it to check if I understood you.

By "deep" SSR, I think you mean SSR similar to how it works in React components. That is, rendering a CE would produce an output that can be displayed before that CE's logic has loaded. This is something you're saying we should not worry about supporting now. That makes sense to me.

By "not deep" SSR, I think you're saying that we just put the CE tag itself into HTML. Without worrying what it resolves to. That makes sense to me too. My question was about _this_ flow though — not about the "deep" SSR.

In particular, https://github.com/facebook/react/issues/11347#issuecomment-713230572 describes a situation where even when we _already have_ the CE logic downloaded, we still can't show anything to the user until hydration. Becomes its properties are unknown until the whole application's JS client code loads and hydration happens. I'm asking whether this is indeed the desired behavior.

I don't think it's a vague concern from my perspective. I don't want to misunderstand the feature request. So getting the exact semantics specified would help me evaluate this proposal. E.g. one practical concern is that React doesn't actually diff attributes/properties in production during hydration today because none of the built-in components need that. But it's something we could add for this particular case if it's really needed.

@gaearon

I think https://github.com/facebook/react/issues/11347#issuecomment-713230572 might not be the best example case in which this feature is needed.

At least in my experience, setting properties rather than attributes is not all that necessary for simpler types like strings, numbers, or booleans.

The case you describe could easily be achieved just using iconname as an attribute directly and it would still be largely the same final rendered result without waiting for hydration.

The custom element implementation loads and it gets registered. If I understand correctly, this is the process called "upgrading". However, even though its JS is ready, it still can't show anything because we have not included the iconname in the HTML. So we don't have the information for which icon to render. This is because we said earlier that "non-builtin properties can be dropped from the HTML". So the user still sees a "hole".

As for what you mention here, depending on how the component is implemented you could avoid this problem of seeing a "hole".

That could be achieved either by just plain setting defaults in this case or by accepting part or even most of the content through slots so that content is displayed even before the component is upgraded.


I think this feature would be mostly useful for using more complex components that have properties which are Objects, Arrays, Functions, etc.

Take for an example, the lit-virtualizer virtual scroller component.

For this to work properly it requires an items array and a renderItem function and you can even optionally set an scrollTarget Element, all of which can only be set in React using refs right now.

For a case like this, you would probably be loading the content with some sort of pagination or lazy loading anyway so not having any content until the hydration step on an SSR case might not be that problematic.

@gaearon Please note that custom elements are a DOM standard and therefore there isn't per se a "WC community" in the sense that everyone who uses custom elements uses them in the same way. Custom elements are integrated in their own way for each developer, as they are inherently a low-level primitive that people build upon.

That said, over time numerous patterns have emerged and all popular frameworks (except React) implement some solution to provide compatibility for this DOM standard. As you can see on https://custom-elements-everywhere.com/, all frameworks/libraries (except React) have chosen different options. In this issue, the chosen options are listed as options 1, 2 and 3. There is no framework/library that currently implements option 4 or 5 and I don't think it is desirable to implement these.

Therefore, I propose that React follows suit with the other frameworks/libraries and chooses an option that has proven working, e.g. out of options 1-3. I don't think React needs to reinvent the wheel here by choosing option 4 or 5, which we have no data for is a longterm maintainable solution for either React or those that build upon DOM standards.

So, by the process of elimination (with the comments linked below), since option 3 is not going to be done to JSX and options 4 and 5 have been marked undesirable by WC people in this thread, we're stuck with 1 or 2.

And since 1 seems like it has untenable drawbacks, it looks like Option 2 is the one? Just go with how Preact does it?


There is no framework/library that currently implements option 4 or 5 and I don't think it is desirable to implement these.

(@TimvdLippe in https://github.com/facebook/react/issues/11347#issuecomment-713474037)

I think it's pretty unlikely that we would introduce new JSX syntax for the sake of this particular feature alone.

(@gaearon in https://github.com/facebook/react/issues/11347#issuecomment-713210204)

That would make sense to me yes. Thanks for the summary! 😄

At least in my experience, setting properties rather than attributes is not all that necessary for simpler types like strings, numbers, or booleans. The case you describe could easily be achieved just using iconname as an attribute directly and it would still be largely the same final rendered result without waiting for hydration.

I'm not quite following. My hypothetical scenario was a response to https://github.com/facebook/react/issues/11347#issuecomment-713223628 which sounded like a suggestion that we shouldn't emit attributes in the server-generated HTML at all, except for id and class. I don't know what "just using iconname as an attribute" means — are you saying you'd like to see the server-generated HTML _include_ other attributes? This seems to contradict what was just said earlier. I think it would really help if each response presented a complete picture because otherwise we're going to keep going in circles. My question to you is:

  1. What exactly gets serialized in the server-generated HTML in the scenario I described
    a. In case <github-icon iconname="smiley" />
    b. In the case you brought up, e.g. <github-icon icon={{ name: "smiley" }} />
  2. What DOM API is called on the client during hydration in either of those cases

Thanks!

Was it considered an option to create a shim, replacing React.createElement() with a custom implementation, which does properties mapping inside?

Usage example:

/** @jsx h */
import { h } from 'react-wc-jsx-shim';

function Demo({ items }) {
   return <my-custom-list items={items} />
}

Draft implementation:

export function h(element, props, children) {
   if(typeof element === 'string' && element.includes('-')) {
      return React.createElement(Wrapper, { props, customElementName: element }, children)
   }
   return React.createElement(element, props, children);

}

function Wrapper({customElementName, props}) {
   const ref = React.useRef();
   React.useEffect(() => {
      for(const prop in props) {
         ref.current[prop] = props[prop];
      }
   });
   return React.createElement(customElementName, { ref, ...props });
}

I know that this is not a built-in feature, but should unblock users with very low overhead, I think.

@just-boris that is basically what apps or libraries do now, with either a createElement patch or a wrapper. You also need to exclude the special-cased property names in React. I don't think there's an exported list, so they just hard code a denylist like ['children', 'localName', 'ref', 'elementRef', 'style', 'className'].

Because it's ref-based, the properties are not set on the server. This is why I call the concern vague. There's no way to set properties at all in React, so it's all broken right now. The workaround available already only work on the client. If there becomes built-in a way to set properties on the client and not on the server, it doesn't suddenly fix or break SSR.

I would actually be more concerned with events than SSR. React doesn't have a way to add event handlers, and that's a huge hole in interoperability with basic DOM APIs.

Was it considered an option to create a shim, replacing React.createElement() with a custom implementation, which does properties mapping inside?

As mentioned by Rob in the original issue, I've experimented with this in the past via https://github.com/skatejs/val, so having a custom JSX pragma that wraps React.createElement is a viable - possibly short-term - solution.

I also have WIP branch of Skate implementing both of what has been referred to as deep and shallow SSR, specifically for React. The learnings from this work thus far, are that you can't have a single JSX wrapper for all libraries if you want to do deep SSR because each library is dependent on their own APIs and algorithms for doing so. (For context, Skate has a core custom element part of its library and small layers built to integrate each popular library on the market to make consumption and usage consistent across the board.)


I don't operate much in open source anymore, and don't do Twitter anymore either, so feel free to take my opinion with a grain of salt.

Option 1 feels like it exists as the devil's advocate for Option 2.

Option 2 feels like the way to go. It's close to what Preact has done, so there's precedent and a possibility of a community standard (which I think would be fantastic to do anyways; JSX has shown this can work), and it's also a pragmatic move for React in being easy to upgrade (as opposed to Option 1).

Option 3 looks good on paper, but I think logistically it would be a lot more difficult than Option 2 due to cognitive overhead, documenting around said overhead, and unknowns with SSR.

As mentioned, I had originally presented option 4 but after reflection, I'm not sure I like it anymore. You could change it to be more opt-in, where you specify explicit props and events (instead of attrs), but even then Option 2 requires less knowledge of implementation details and there's nothing for anyone to have to change to adopt it.

Option 5 feels like we'd be ignoring the core issue entirely.


As an aside, I'm super happy to see traction on this and hope y'all have been doing well!

If there is a possible solution in the userland, can it be consolidated and recommended by the community for the time being?

It would be much easier to convince the React core team to add a feature if it already exists as a popular and viable 3rd-party module.

If there is a possible solution in the userland, can it be consolidated and recommended by the community for the time being?

I'm don't believe this is something that can be [neatly] patched from the outside. Most userland solutions are wrappers such as this and this. Without modifying React, I can't think of a reasonable alternative to this approach, and wrapping is definitely not an ideal solution. 😕

Note that in vuejs/vue#7582, Vue chose to use a sigil, and they chose "." as a prefix (not Glimmer's "@").

I don't have a link to an authoritative statement on this (and maybe someone closer to Vue can explicitly confirm), but it appears that Vue has moved to Option 2 as of Vue 3.0, meaning that it behaves similarly to Preact. (See this issue for context.)

Because it's ref-based, the properties are not set on the server. This is why I call the concern vague. There's no way to set properties at all in React, so it's all broken right now. The workaround available already only work on the client. If there becomes built-in a way to set properties on the client and not on the server, it doesn't suddenly fix or break SSR.

Can you please respond to the specific questions in https://github.com/facebook/react/issues/11347#issuecomment-713514984? I feel like we're talking past each other. I just want to understand the whole intended behavior — in a specced way. What gets set when, and what gets serialized in which case.

As a random Preact+WC dev who stumbled here, I wanted to point out that "Option 2" is not only a _breaking change_, but also an _incomplete solution_ (Preact can still only interact declaratively with a subset of valid Web Components).

Even as a current user of the heuristic, that sounds hardly desirable in the long run.


Preact's heuristic ("option 2") is unable to set properties that have special meaning in React (key, children, etc.) or Preact (anything starting with on).

I.e. after rendering below JSX:

<web-component key={'key'} ongoing={true} children={[1, 2]} />

properties key, ongoing and children will all be undefined (or whatever the default value is) on the created instance of web-component.

Also, contrary to the OP, this option is also a breaking change. Most web components (e.g. made with lit-element) offer ability to pass serialized rich data via attributes, for purpose of being used with pure HTML. Such components can be rendered from React like so

<web-component richdata={JSON.stringify(something)} />

which will break if "option 2" is chosen. Not sure how common such use of web components is in React, but there are definitely some code bases that do so, it's less efficient then refs but also terser.

Finally, Web Component's author is free to attach subtly different semantics to attribute and property of the same name. An example would be a Web Component mimicking native input element's behavior - treating attribute value as initial value, and only observing changes on the property value. Such components can't be fully interacted with via jsx with the heuristic approach, and also could experience a subtle breakage if it was introduced.

I think it's pretty unlikely that we would introduce new JSX syntax for the sake of this particular feature alone.

What about using namespaces? https://github.com/facebook/jsx/issues/66 proposes to add a react namespace for key and ref. Along the same lines, would something like react-dom be an ok namespace to mint for identifying DOM properties? Using the OP's option 3 example, that would be:

<custom-img react-dom:src="corgi.jpg" react-dom:hiResSrc="[email protected]" width="100%">

That's not the most ergonomic. Just dom would be better, but not sure if React wants to mint namespaces that don't start with react. Also, poor ergonomics on this isn't the end of the world, if the idea is that setting as properties should be the less common case. Using attributes is better, since that provides parity with SSR. Setting as properties is for when attributes are problematic (such as to pass an object or for properties not backed by attributes), and it is precisely for those problematic properties that we can't SSR them to attributes anyway, and would need to hydrate them via JS.

an _incomplete solution_

(Preact can still only interact declaratively with a subset of valid Web Components)

@brainlessbadger can you also edit your comment above to include what this subset actually is? Eg. which Web Components / Custom Elements are affected (with examples)? And how exactly they are affected?

@karlhorky I added an explanation. Sorry for being needlessly vague.

Regarding Web Components events. Is there any consensus yet around how to avoid future blocking by colliding names of events?

This is a problem for attributes and properties too since new attributes can be added to HTMLElement like all of these: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes Looks like there are a couple of newly added ones too so there are still new ones being added.

I think strictly speaking you should have to use a - in the name of any custom attribute or custom event.

Anything else is a compromise weighing the risks of that component blocking a future spec from using a nice name.

Events makes me extra nervous though because it's not just a risk of that component having incompatible behavior. If the new event bubbles, it'll trigger all existing Web Component in that tree path.

If custom attributes and events were required to use a - in their name that could be used as a heuristic to determine what to use. Then we'd special case the built-in ones. That's already how we know if it's a custom element in the first place.

Properties could then be used as conveniences to provide a nicer short hand. At least those can shadow the built-in properties and not collide with future properties.

I think strictly speaking you should have to use a - in the name of any custom attribute or custom event.

The custom element spec doesn't include such restrictions, so enforcing them in React will significantly affect interoperability.

Custom element authors shouldn't be expected to subscribe to a framework's arbitrary requirements. Imagine every framework doing this with varying opinions and conventions. This defeats the purpose of using web components. 😕

Ideally, the framework should adapt to accommodate the standard.

Indeed. setAttribute() and dispatchEvent() are just non-web-component specific APIs that have allowed arbitrary names since their inception. Any element can have any attribute and fire any event - it's just a ground truth of the DOM.

The data- namespace was added for a reason though. There’s a difference between what you technically could do and what’s best practice. You can technically patch prototypes too but then you get contains/includes situations.

I wouldn’t suggest that just React adds this but that the community shifts to this convention to help protect the future namespace. I just think it’s unfortunate that this issue isn’t taken seriously. But perhaps the web is doomed to only add awkward names to new APIs.

I like the idea of just excluding non-builtins from SSR and prefer properties during hydration because with declarative shadow DOM the real content could be expanded inside.

Concretely though I believe that will cause problem for the current approaches people use with SSR + AMP since that uses attributes and you can assume that the AMP runtime has already loaded.

Assuming the door isn't closed on option 3, it has a significant advantage over option 2, not listed in the cons above -- The problem I've found with option 2 is that if web component libraries are loaded asynchronously, preact might not know, for an unknown element, that a "property" will be a property. Since it doesn't find the property existing in the unknown element, it defaults to an attribute. A work around is to not render that component, until the dependent web component libraries are loaded, which isn't very elegant (or ideal performance-wise). Since my group uses asp.net, not node.js, I've never explored SSR really, so the observations below are speculative.

It's a nice gesture, I think, on the React team's part, to suggest going above and beyond what other frameworks support (to my knowledge), as far as SSR, allowing object properties to be passed down during initial rendering, which could serve the user better.

I don't know if I'm stating the obvious or not, but I think there is a bit of a consensus with web components that for some properties, it is convenient to sometimes set the initial value of the attribute via a JSON string, wrapped in single quotes.

AMP uses another convention, though -- it uses script type=application.json to do this, which is a reasonable (but verbose) alternative.

But sticking with the attribute approach:

The web component library can then parse the attribute, or be passed in the value via a property.

So to avoid hydration delays it would be convenient to set:

<github-icon icon='{"name":"smiley"}'></github-icon>

during SSR. Then github-icon could immediately do its thing, internally do a JSON.parse of the attribute, and not have to wait for React to pass in the (more complete?) object value.

So now we have a tricky situation -- if rendering on the server, we want "icon" to be treated as an attribute, but be able to pass in the value as an object, which ideally the SSR mechanism would stringify into the attribute. On the client we want to be able to pass in the object directly, and not go through the overhead of stringifying and parsing.

Of course, some properties are objects that aren't amenable to stringifying/parsing, so this doesn't apply, and those properties would need to wait for hydration to complete.

If all properties and attributes followed the React team's preferred naming convention (perhaps) -- all attributes had dashes, and all properties were compound names using camelCase, perhaps the existence of a dash could help distinguish between attributes and properties:

<github-icon my-icon={myStringifiedProperty} myIcon={myObjectProperty}></github-icon>

The problem is nothing in the syntax above indicates what to do on the server versus the client, and ideally we would be able to use one binding for both, and React would be smart enough to figure out which one to use.

React could just assume that if an attribute/property key is camel case, to always pass it as an object on the client side, and a JSON-stringified serialization of the object if it's set during SSR. Likewise, dashes in the key could (safely, I think) assume it is an attribute, and just pass in the .toString() of the value. But I think this assumes too much. And not supporting single word attributes and properties, which is considered valid HTML if the attributes are applied to a web component extending HTMLElement, would be too constricted. I'm in favor of W3C issuing a list of "reserved" attribute names it might use in the future, similar to reserved key words for JS, and frameworks finding ways of warning developers if they are improperly using a reserved attribute name.

But I think option 3 is the best approach. However, if it can be enhanced, as suggested by @gaearon, for an even better user experience, that would be great.

My suggestion would be:

<github-icon icon={myDynamicIcon}/>

means attribute (toString()).

<github-icon icon:={myDynamicIcon}/>

would mean -- during SSR, ignore, but bind on the client to the object property (after hydrating).

Now what about the scenario (some of the?) React team is interested in solving? My first thought was just some other sigil, such as two colons:

<github-icon icon::={myDynamicIcon}/> //Not my final suggestion!

would mean, during SSR, JSON.stringify the property, set as an attribute in the rendered HTML, and pass as an object on the client when the property it is binding to changes after hydration.

This leaves the tricky situation of what to do with compound names. I.e. if we set:

<github-icon iconProps::={myDynamicIconProp}/>  //Not my final suggestion!

it wasn't controversial in the past that the corresponding attribute for property name iconProps should be icon-props.

Perhaps this has become more controversial, because of the specter that some of these web components could, with no changes, become built into the platform, where attributes can't have dashes (but properties can be camelCase). To my knowledge, there is not yet a native element that allows passing in complex objects via JSON deserializing, but I wouldn't be surprised if the need arises in the future. So how would React know when to insert dashes or not?

The only (ugly?) suggestion I can come up with is:

<github-icon icon-props:iconProps={myDynamicIconProp}/>

which means, on the server, use attribute icon-props after serializing, and on the client use property iconProps, passing in objects directly.

Another (long shot?) potential benefit of the more verbose notation, allowing for a hybrid attribute / property pair, is this: It might be a little faster to repeatedly set the properties of a native element, rather than the corresponding attribute, especially for properties that aren't strings. If so, does React currently use attributes on the server, and properties on the client? If not, is it because of the same issue of naming translation difficulties, which this notation would solve?

@bahrus I think it can be simplified

<my-element attr='using-quotes' property={thisIsAnObject} />

I think it might be best to illustrate the issue with a concrete example.

The HTML Form element has a number of attributes. The ones with "compound names" don't all seem to follow a consistent naming pattern -- generally it sticks with all lower case, no separators, but sometimes there's a dash separator -- "accept-charset" for example, sometimes not -- "novalidate".

The corresponding JS property name doesn't fit into a nice universal pattern either -- acceptCharset, noValidate. The noValidate property / novalidate attribute is a boolean, so on the client, it would be wasteful (I imagine) to do myForm.setAttribute('novalidate', '') as opposed to myForm.noValidate = true.

However, on the server, it doesn't make sense to set myForm.noValidate = true, as we want to send a string representation of the DOM, so we need to use the attribute:

<form novalidate={shouldNotValidate}>

Actually, it isn't clear to me that React/JSX has a universal way of setting a boolean attribute conditionally, relying, perhaps, on a fixed, static lookup table, which seems(?) overly rigid. If I may be so bold, this seems like another area React/JSX could improve, to be fully compatible with the (messy) way the DOM works, as part of this RFC?

How can we represent all these scenarios, so that the developer has full control on the server (via attributes) and client (via properties), without sacrificing performance? In this case of a boolean like novalidate, I would propose:

<form novalidate:noValidate?={shouldNotValidate}/>

If React fully supported the DOM, messy as it is, in as performant a way as possible, I think that would go a long way to support custom elements, which also are probably not very consistent in naming patterns.

Adding support for optionally serializing object properties to attributes via JSON.stringify on the server, would be an additional huge win-win for React and web components, it seems to me.

Or am I missing something? ( @eavichay , how would you suggest these scenarios best be represented, not following from your example).

Was this page helpful?
0 / 5 - 0 ratings