-
Notifications
You must be signed in to change notification settings - Fork 558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add create-custom-element-type RFC #15
Conversation
Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need the corporate CLA signed. If you have received this in error or have any questions, please contact us at [email protected]. Thanks! |
it's easiest to convert camel cased properties to lowercase attributes, without | ||
a delimiter, when calling `ReactDOMServer.renderToString()`. | ||
|
||
## 2. ReactDOM.createCustomElementType() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not call it just createCustomElement
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion. This was the name suggested by @gaearon when we were first kicking around the idea. facebook/react#11347 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, I think it would be a little misleading to call it createCustomElement
since what it returns is not actually a custom element, but instead a React component that wraps a custom element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe createCustomElementWrapper in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.createCustomElementTypeWrapper()
?
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks! |
} | ||
|
||
return ( | ||
<XFoo longName={name} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having some ideas now that I've seen this part specifically. The 95% use-case is probably that you're going to be able to have access to the custom element constructor where you're writing the React code. The rest of the time, you may want deferred upgrades.
Part 1 - you have the custom element constructor
If you have the custom element constructor, you actually don't need to define a wrapper or any information about it. You have the properties it defines as well as a way to diff and construct it if it needs to be patched.
In this scenario, React could internally just check if it's a custom element constructor and set props if defined, or attributes otherwise. They don't need the tag name, either.
I PR'd this to Preact awhile back for reference: preactjs/preact#715.
SSR-story
This assumes you have server-side support for whatever DOM features would be utilised (JSDOM / Undom / Domino, etc). If this is not the case, then it should fallback to Part 2.
Until there's support for declarative shadow DOM (whatwg/dom#510) custom elements that have shadow DOM would render the host + light DOM as it normally would with out shadow DOM (no special APIs to mention here, just the standard childNodes
stuff). This requires JavaScript on the client to initialise and rehydrate the custom elements, but this is no different that what Part 2 is offering on the client.
Due to the tight coupling this part has with the DOM, it is assumed that it would be implemented as part of ReactDOM, as opposed to React.
Part 2 - deferred upgrades (no constructor)
You do what you're proposing in this RFC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case of "Part 1" how would server-side rendering work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'd have to have JSDOM support or another server side implementation support, but it would render out to host + light DOM and then rehydrate on the client. I was making the assumption this would be implemented in react-dom, if possible. I'll update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@treshugart hm I'm confused by some of your comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ugh, hate the new github behavior where shift-enter submits a comment instead of inserting a newline. Continuing on from above...
In your comment you start off by talking about Part 1 and using the element constructor. But Part 1 of my proposal assumed someone was just using the tag name <x-foo>
. So you're commenting on a somewhat different use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you're commenting on a somewhat different use case.
Sure, but what I'm saying is that most of the time you have access to the constructor; this is the happy path and I think it should be the primary consideration by this RFC. I believe itt enables better ergonomics because you can infer all your information from the constructor. Manual configuration is necessary when you don't have it, but it might be worth considering making this secondary.
There would be no attempt by the custom element to render its shadow dom content server side. I'm assuming that would all boot-up client side.
Ok, that's fair. Looking back I think this would naturally happen anyways if using react-dom and it was being run under a node-dom env. Let's not worry about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@treshugart Are you suggesting folks would never use their custom element tag name and would instead always use the constructor in their render functions? That's not actually something I would want to do tbh.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, our ideals here are quite divergent. I was hopeful that using a custom element in React could be the same way you'd use a React component. I almost never use custom element tag names and it's been quite liberating. Sure in HTML you do, but this isn't HTML.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm concerned that if you're using the constructor, it may be tough to distinguish a custom element from a React component. Because they are different and may have subtle differences in their behavior, it seems like if you want something to be indistinguishable from a React component it may be better to use the ReactDOM.createCustomElementType()
API to wrap the custom element in an actual React component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's fair. I'm 100% happy with this RFC as is and am super stoked you're doing it. I really hope this gets in.
As an aside, and in terms of ergonomics, I think it would be a nice addition to eventually add support for constructors because it clean up the API, essentially making the usage of a custom element just as simple using a React component. The fact that two components can have inconsistencies isn't a WC / React thing, it's a component author thing and I think that's an orthogonal problem.
I believe that this rfc would accommodate the needs of StencilJS. We would like to see this move forward because we will want to use it to bring Ionic Components to the React ecosystem. |
Does anyone use SSR in conjunction with WC? Or are there plans to? If there are I would heavily lean towards attributes by default because doing so allows React SSR to behave the same way as React on the client out of the box. |
Not really. The hard part is figuring out how to represent Shadow DOM (and the distribution that it does) in some form that doesn't require JavaScript. There has been a lot of discussion on this topic and some interesting experiments, but it's definitely not mainstream to SSR them.
If React needs to pass an object or array to the element, what would happen in this case? |
I was thinking that if that were the case you'd have to write the custom attribute/property mapping. |
I'd like to avoid getting stuck in a situation where SSR + WC is a thing but it's abnormally difficult in React due to past decisions of ours. If that's vanishingly unlikely to happen then it's less of a concern. |
You can't use refs to set properties in SSR and in SSR it's expected that all you can do is set attributes. From the perspective of SSR a |
The pitfall is if we make React use properties for WC, then you turn on React SSR, your app won't behave properly if those WC don't treat attributes and properties identically. |
Sure, but if a WebComponent doesn't have attribute<->property mapping then you inherently cannot use SSR in any situation. It's not so much a pitfall as an expected platform limitation that is the fault of the WebComponent implementation. And it doesn't make sense to punish those using the majority of well designed WebComponent implementations that do implement attribute<->property mapping because there are a smaller subset of WebComponents that don't behave properly. |
Is React aware of when it's doing SSR versus running on the client? In the RFC I had a suggestion that if ReactDOMServer.renderToString() is running it could always use attributes. Are there other ways to do SSR in React other than that API?
I think most custom elements that take arrays or objects are doing so in order to render content into their shadow roots. But that wouldn't work in an SSR situation anyway, so those components would probably have to be booted up on the client regardless. From my perspective, if I have an entirely client-side app that uses React and web components, it might be annoying to have to write a lot of wrappers just to make it work. |
Certainly we are. (In fact it's two mostly-distinct codebases.) But as much as possible we'd like your app to work perfectly with SSR if it works on the client. If we use attributes on the server and properties on the client that creates a pitfall where the two modes might do totally different things, which is a good way to introduce bugs. |
Yes, there's @skatejs/ssr and StencilJS has something as well. |
@treshugart and @adamdbradley for their thoughts. Adam - does the stencil SSR middleware allow the use of shadow dom? |
@sophiebits What's the existing SSR behavior in React for something like My (probably naïve) suggestion would be to match whatever behavior React uses in this situation. I recognize the concern around not knowing if the property is supported, but it's equally possible for someone to write a custom element that doesn't support attributes. If the author doesn't add the attribute to their edit: I'm basically just repeating @dantman's points, so +1 to his comments 😁 |
Looks like |
oh interesting. Is it the case for any HTML attribute/property pair that if it was first rendered with SSR it will always continue to use the attribute and never the property? Or are there instances where it uses the attribute for the server rendered string, but switches to the property once the client boots up? |
No, we always use the tabindex attribute regardless of if the initial render was SSR. |
oh, sorry, I think my question was a bit unclear. I was wondering if there was any precedent where React uses an attribute on the server and then switches to using the property on the client. Maybe the behavior for checked would be an example of doing that? |
sorry for the double post, something else just occurred to me. If I have a property,
And I go to SSR that, will it just return One possibly nice aspect of using attributes on the server and properties on the client would be if my element had attribute<->property parity then I could do:
and that should "just work" regardless of if I'm on the client or server. Whereas the ref example seems like it might break? |
Yes, components are not mounted on the server. Any properties set by ref or by React (instead of being mapped to attributes) must be set on the client during hydration. They can't be set by the server. |
@robdodson At this time no, Stencil does not use shadow dom with SSR.
I'd like to clarify that custom elements and the use of shadow dom are two different things for stencil. All components use custom elements, but shadow dom is an opt-in feature (the slot API is polyfilled if slot is used w/out shadow dom). However, if I'm following this discussion the use of shadow dom shouldn't matter either way, it's more about passing data to custom elements. As far as passing data during SSR, it's all being pass as through attributes, which are then translated to their properties on the SSR. When the html hydrates client side it's essentially doing the same thing. Default is the attribute |
Is this asking whether there are cases where WebComponents are used within projects that do some SSR? Or whether the WCs themselves need to have their attributes SSR'd? For the former, I think certainly there must be. For the latter, I can see it being useful to SSR HTML-defined attributes ( |
Actually, thinking about this more, are there cases where a WC is implemented in a way that requires a custom attribute to already be defined at construction time? In which case, would client-side hydration of the property occur too late? Or, would React be able to reliably hydrate the properties prior to the WC's constructor running? |
They require the WC JS, but do not require the React JS to function if the data can be expressed in simple attributes. It's not the optimal use, but remember that React can be used server side to render traditional html pages that are not hydrated on the client. And WebComponents still work in this context, i.e. to my knowledge you can make a traditional |
If the consensus is that all "responsible" CE/WC implementations treat properties and attributes exactly the same when assigned a primitive value (and the name/capitalization always matches exactly) then I guess I am open to defaulting to properties. It may still be surprising that passing an object works on the client and not on the server though. |
Can we make it work on the server? Can the SSR logic be that for object values to call JSON.stringify()? That would be consistent with Polymer's approach to WC. As it is, per HTML spec, SSR logic needs different serialization logic for Booleans, so if it already needs to check type, why not also check for object? |
It may depend on the DOM implementation being used for SSR. As @adamdbradley mentioned, if you are using custom elements without shadow dom, then you can run it all through something like puppeteer which would properly trigger the lifecycle callbacks for custom elements, and handle setting properties.
One issue to be aware of is that you lose object identity, so if one of the values I was trying to pass was a reference to another object or a DOM node, that would then be broken by the stringify step. Usually when I'm passing a custom element an object or an array I'm expecting it to use that to render more children. If it can't actually boot the custom element up on the server (depending on how SSR is done) then it might be of limited value. But I don't personally have a strong opinion either way. |
I might be missing something, but it seems that every react prop for |
One of the best things about Server Side React is that if JavaScript fails you can serve a good fallback experience for users if you are following progressive enhancement. A progressive web component looks like the following: <custom-autocomplete>
<select>
<option>
<option>
<option>
</select>
</custom-autocomplete> Here if JavaScript failed we can fallback to a select input for the user, this is the same way the the datalist element works. It would be a awesome if any decision around this proposal would make it easier to do progressive enhancement with Web Components. |
Looks like there's two possible routes but both have problems:
Seems like maybe react doesn't need to worry about it. If the React component does what it needs to do to the CE in Yes, true, the whole app may not be visibly rendered fully at first, but that's an issue for the CE user because there's no way React can handle all the cases. |
Oh, but then there's the Personally I'm in favor of JSX syntax for specifying something be passed as a prop. f.e. |
Another thing that would help is for React docs to say in the SSR docs:
The person implementing SSR in a project probably has the authority or access to make sure such an advice is followed. A monkey patch of |
I'd love to see this being implemented. You're doing very valuable work. |
I second this! A pity to see this stagnate though. @robdodson Do we know the status this RFC, beyond the lack of discussion for the last year? Any roadmap where it's considered? |
I posted this over in the React issue #7901, but it might make sense here, too. If it doesn't, I apologize for the spam: OK, since there hasn't been any engagement on this, I want to take a first shot at syntax for implementing DOM events and properties. For the sake of clarity, I want to define properties which should not be confused with HTML attributes. DOM properties exist on the DOM object (imagine running Obviously for most cases the traditional React way of interacting with the DOM through props would be preferable, I am choosing to use Option 1 — DOM props as objectsThis is fairly straightforward: Treat DOM events and properties as objects similar to how one would pass in <button
domEvents={{
click: someCallback,
mouseenter: e => someOtherCallback(e.x)
}}
domProps={{
disabled: someCondition
}}
>Click me!</button> Option 2 — DOM props as individual React propsThis would likely require the use of a sigil to minimize the risk of colliding with props in the wild. Here we can borrow the syntax from something like lit-html or Angular (I prefer Lit), or we could use a sigil like <button
@click={ someCallback }
@mouseenter={ console.log }
.disabled={ someCondition }
>Click me</button> Option 3 — Prop key exported from React DOMSince this feature would be strictly DOM-based, there could be a key function exported from React DOM that allows interfacing with the DOM events and properties more efficiently. Like the above, this would be necessary to minimize the risk of collisions with keys in the wild. import { domEvent, domProp } from 'react-dom';
// ...
<button
[domEvent('click')]={ someCallback }
[domProp('disabled')]={ someCondition }
>Click me</button> Option three feels clumsy and as far as I know React's JSX doesn't currently allow that kind of computed prop key. Option one feels very React like, but a bit too verbose for my liking. Personally I prefer option two, as it is familiar from users of other view engines and feels React-like enough as not to be a total departure from established norms. |
FWIW, those examples are boolean attributes (except However, non-boolean attributes (which will be the more likely case with these attributes) tend to use a hyphen like |
I don't think any of those three attributes are boolean attributes.
That's a good question. I don't know the backstory, but I've always assumed the prefix for data-* and aria-* were to help create a sort of namespace. data-* meaning "custom and defined by the developer" and aria-* meaning "specifically relating to accessibility". I haven't seen any new attributes that include this namespacing approach though... I mostly suggested the all lowercase with no separator approach because Polymer tried to do attribute to property mapping using the dash separator, and it was often confusing for developers. Reducing the amount of "magic" that the framework does seemed like a useful thing in my opinion. Btw I wanted to mention that I've seen a lot of replies to this thread but I haven't personally participated much because it doesn't seem like there's much interest from the React team in this proposal. I wasn't sure if it was worth continuing to iterate on it if they were either not interested or thinking of going in a different direction. |
This proposal was part of the intake and research that @josepharhar has been doing in facebook/react#11347 (comment). I'm going to close this, since it's already outdated, but I expect @josepharhar will submit an updated RFC based on his findings. |
RFC to address facebook/react#11347.
The changes in this RFC specifically address how React passes data to custom
elements and listens for their DOM events.
This RFC has two parts:
React would change its current behavior such that when it passes data to a
custom element, it uses JavaScript properties instead of calling
setAttribute
.React would add a new API,
ReactDOM.createCustomElementType()
which wouldcreate a wrapper React component that knows how to map properties and event
handlers back to a custom element.
cc @developit @treshugart @effulgentsia @justinfagnani