facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
225.32k stars 45.94k forks source link

RFC: Plan for custom element attributes/properties in React 19 #11347

Open robdodson opened 6 years ago

robdodson commented 6 years ago

This is meant to address #7249. The doc outlines the pros and cons of various approaches React could use to handle attributes and properties on custom elements.

TOC/Summary

Background

When React tries to pass data to a custom element it always does so using HTML attributes.

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

Pros

Non-breaking change

It is possible to create a custom element that only uses attributes as its interface. This authoring style is NOT encouraged, but it may happen regardless. If a custom element author is relying on this behavior then this change would be non-breaking for them.

Cons

Developers need to understand the heuristic

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

Falling back to attributes may conflict with future globals

Sebastian raised a concern that using in to check for the existence of a property on a custom element might accidentally detect a property on the superclass (HTMLElement).

There are also other potential conflicts with global attributes discussed previously in this doc.

Option 3: Differentiate properties with a sigil

React could continue setting attributes on custom elements, but provide a sigil that developers could use to explicitly set properties instead. This is similar to the approach used by Glimmer.js.

Glimmer example:

<custom-img @src="corgi.jpg" @hiResSrc="corgi@2x.jpg" width="100%">

In the above example, the @ sigil indicates that src and hiResSrc should pass data to the custom element using properties, and width should be serialized to an attribute string.

Because React components already pass data to one another using properties, there would be no need for them to use the sigil (although it would work if they did, it would just be redundant). Instead, it would primarily be used as an explicit instruction to pass data to a custom element using JavaScript properties.

h/t to @developit of Preact for suggesting this approach :)

Pros

Non-breaking change that developers can opt-in to

All pre-existing React + custom element apps would continue to work exactly as they have. Developers could choose if they wanted to update their code to use the new sigil style.

Similar to how other libraries handle attributes/properties

Similar to Glimmer, both Angular and Vue use modifiers to differentiate between attributes and properties.

Vue example:

<!-- Vue will serialize `foo` to an attribute string, and set `squid` using a JavaScript property -->
<custom-element :foo="bar” :squid.prop=”ink”>

Angular example:

<!-- Angular will serialize `foo` to an attribute string, and set `squid` using a JavaScript property -->
<custom-element [attr.foo]="bar” [squid]=”ink”>

The system is explicit

Developers can tell React exactly what they want instead of relying on a heuristic like the properties-if-available approach.

Cons

It’s new syntax

Developers need to be taught how to use it and it needs to be thoroughly tested to make sure it is backwards compatible.

Not clear how server-side rendering would work

Should the sigil switch to using a similarly named attribute?

Option 4: Add an attributes object

React could add additional syntax which lets authors explicitly pass data as attributes. If developers do not use this attributes object, then their data will be passed using JavaScript properties.

Example:

const bar = 'baz';
const hello = 'World';
const width = '100%';
const ReactElement = <Test
  foo={bar} // uses JavaScript property
  attrs={{ hello, width }} // serialized to attributes
/>;

This idea was originally proposed by @treshugart, author of Skate.js, and is implemented in the val library.

Pros

The system is explicit

Developers can tell React exactly what they want instead of relying on a heuristic like the properties-if-available approach.

Extending syntax may also solve issues with event handling

Note: This is outside the scope of this document but maybe worth mentioning :)

Issue #7901 requests that React bypass its synthetic event system when declarative event handlers are added to custom elements. Because custom element event names are arbitrary strings, it means they can be capitalized in any fashion. To bypass the synthetic event system today will also mean needing to come up with a heuristic for mapping event names from JSX to addEventListener.

// should this listen for: 'foobar', 'FooBar', or 'fooBar'?
onFooBar={handleFooBar}

However, if the syntax is extended to allow attributes it could also be extended to allow events as well:

const bar = 'baz';
const hello = 'World';
const SquidChanged = e => console.log('yo');
const ReactElement = <Test
  foo={bar}
  attrs={{ hello }}
  events={{ SquidChanged}} // addEventListener('SquidChanged', …)
/>;

In this model the variable name is used as the event name. No heuristic is needed.

Cons

It’s new syntax

Developers need to be taught how to use it and it needs to be thoroughly tested to make sure it is backwards compatible.

It may be a breaking change

If any components already rely on properties named attrs or events, it could break them.

It may be a larger change than any of the previous proposals

For React 17 it may be easier to make an incremental change (like one of the previous proposals) and position this proposal as something to take under consideration for a later, bigger refactor.

Option 5: An API for consuming custom elements

This proposal was offered by @sophiebits and @gaearon from the React team

React could create a new API for consuming custom elements that maps the element’s behavior with a configuration object.

Pseudocode example:

const XFoo = ReactDOM.createCustomElementType({
  element: ‘x-foo’,
  ‘my-attr’: // something that tells React what to do with it
  someRichDataProp: // something that tells React what to do with it
});

The above code returns a proxy component, XFoo that knows how to pass data to a custom element depending on the configuration you provide. You would use this proxy component in your app instead of using the custom element directly.

Example usage:

<XFoo someRichDataProp={...} />

Pros

The system is explicit

Developers can tell React the exact behavior they want.

Non-breaking change

Developers can opt-in to using the object or continue using the current system.

Idiomatic to React

This change doesn’t require new JSX syntax, and feels more like other APIs in React. For example, PropTypes (even though it’s being moved into its own package) has a somewhat similar approach.

Cons

Could be a lot of work for a complex component

Polymer’s paper-input element has 37 properties, so it would produce a very large config. If developers are using a lot of custom elements in their app, that may equal a lot of configs they need to write.

May bloat bundle size

Related to the above point, each custom element class now incurs the cost of its definition + its config object size.

Note: I'm not 100% sure if this is true. Someone more familiar with the React build process could verify.

Config needs to keep pace with the component

Every time the component does a minor version revision that adds a new property, the config will need to be updated as well. That’s not difficult, but it does add maintenance. Maybe if configs are generated from source this is less of a burden, but that may mean needing to create a new tool to generate configs for each web component library.

cc @sebmarkbage @gaearon @developit @treshugart @justinfagnani

robdodson commented 6 years ago

I really like this approach :)

robdodson commented 6 years ago

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.

gaearon commented 6 years ago

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

sophiebits commented 6 years ago

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.

robdodson commented 6 years ago

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

treshugart commented 6 years ago

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?

sophiebits commented 6 years ago

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

trusktr commented 6 years ago

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,

Here's universal rules we all agree on:

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:

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

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

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,

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

Happy new year!

robdodson commented 6 years ago

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.

trusktr commented 6 years ago

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

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.

robdodson commented 6 years ago

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

trusktr commented 6 years ago

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.

sebmarkbage commented 6 years ago

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.

trusktr commented 6 years ago

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

treshugart commented 6 years ago

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.

jfhector commented 5 years ago

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

volmerf commented 5 years ago

Any update on this RFC?

BBKolton commented 4 years ago

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

brion-fuller commented 4 years ago

Any update on this issue?

mgolub2 commented 4 years ago

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

equinusocio commented 4 years ago

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

trusktr commented 4 years ago

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?

robdodson commented 4 years ago

@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

trusktr commented 4 years ago

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

trusktr commented 4 years ago

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

trusktr commented 4 years ago

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?

TehShrike commented 4 years ago

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.

Lodin commented 4 years ago

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

BBKolton commented 4 years ago

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

matsgm commented 4 years ago

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-

BBKolton commented 4 years ago

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

nojvek commented 4 years ago

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 ^

eavichay commented 4 years ago
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

TimvdLippe commented 3 years ago

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

gaearon commented 3 years ago

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.

TimvdLippe commented 3 years ago

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.

effulgentsia commented 3 years ago

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.

gaearon commented 3 years ago

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

dantman commented 3 years ago

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')]="corgi@2x.jpg" width="100%">
gaearon commented 3 years ago

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

justinfagnani commented 3 years ago

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.

gaearon commented 3 years ago

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

dantman commented 3 years ago

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

claviska commented 3 years ago

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

gaearon commented 3 years ago

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

justinfagnani commented 3 years ago

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

gaearon commented 3 years ago

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?

gaearon commented 3 years ago

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?

yananym commented 3 years ago

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

gaearon commented 3 years ago

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?