facebook / react

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

[React 19] Support scoped custom element registries (i.e, react with Custom Elements being rendered in a shadow root) #28938

Open michaelwarren1106 opened 6 months ago

michaelwarren1106 commented 6 months ago

Summary

This is not an issue, but a (hopefully small) feature request/enhancement to the Custom Element support coming in React 19.

The Custom Element support in React 19 is awesome! But even once React 19 lands, there will still be one challenge that Custom Element component library maintainers face. There is no way to inform React to use a scoped custom element registry when creating elements.

Scoped Custom Element Registries

Proppsal

The above proposal implements a new CustomElementRegistry() api where Custom Element authors can register custom elements to a registry that is not the global registry. An implementation of scoped custom element registries is in Chrome Canary under a flag at the moment, and the Web Components Community Group is working to see them implemented in all browsers.

There are also several polyfills that add this behavior to browsers in the meantime. Here is the one I use:

Polyfill

Use case: Micro-frontend application

Micro-frontend application architectures are very common. This pattern is where a host application will use module federation (webpack, vite etc) to dynamically fetch JS modules containing separate "remote applications" that the host app then assembles into a single seamless application for the user. The architecture is popular because it enables autonomy of releases between remote applications.

For Custom Elements though, the problem is that with only the single global Custom Element registry in browsers, remotes can't scope their Custom Elements efectively and so there can be version conflicts when remote applications depend on different version of the same custom element. Whichever version gets registered first "wins" and the other remotes break.

Scoped element registries solve this. If a remote application is rendered inside a shadow root (custom element, or just a div with an attached shadow root) then all Custom Element definitions in that remote can be pulled from a scoped registry instead of the global one. Bye bye version conflicts! Every remote gets its own version of the Custom Elements it uses.

Feature Request

If the remote application uses a framework like React where document.createElement is used. In order to create a scoped Custom Element using the scoped definition in the custom registry, shadowRoot.createElement() is the mechanism.

React (and other frameworks also) have no way to be informed that they are being rendered inside a shadow root with a scoped registry attached. And therefore have no mechanism to switch from document.createElement to shadowRoot.createElement. React can already render inside a shadow root just fine. React's root can be an element inside a shadow root with no issues. This feature is about detecting whether or not React's render root has a parent element that is a shadow root and switching to shadowRoot.createElement() accordingly.

I hope this is enough detail to describe the ask. I would think that this might not be a huge feature or change to existing React fundamentals, but I'm not an expert on what the exact implementation approach should be. I'm just familiar with the current problem landscape and our workarounds that we use to get around it being cumbersome.

This comment in a thread about the topic has a rough idea of what the implementation might look like.

I would think that with an implementation similar to the above, document.createElement() would be used in applications where there aren't any scoped element registry polyfills, or where the feature hasn't yet landed natively in browsers, so there may be low risk to a check for "is the root node in a shadow root".

mayank99 commented 6 months ago

Correct me if I'm wrong, but your current suggestion wouldn't fully handle all cases. The main assumption you've made is that React's root is created inside the shadow-root, which is not always desirable. More often than not, I want a single React root, so that I can share context between shadow-roots.

One way to fix this would be by treating createPortal() similarly to createRoot() and hydrateRoot(). In all cases, React should automatically use getRootNode(el).createElement().

This should probably be done before React 19 ships as stable, otherwise we might have to wait several years because of behavioral differences/risks with making this change in a minor release.

github-actions[bot] commented 3 months ago

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

mayank99 commented 3 months ago

stalen't

michaelwarren1106 commented 3 months ago

Correct me if I'm wrong, but your current suggestion wouldn't fully handle all cases. The main assumption you've made is that React's root is created inside the shadow-root, which is not always desirable. More often than not, I want a single React root, so that I can share context between shadow-roots.

Im definitely assuming a few things and not covering all use cases. I'm not an expert on the differences between createRoot and hydrateRoot tbh.

If react maintainers would chime in here with their thoughts I'm sure a robust solution can be found that covers the use cases correctly. I just made the issue to represent the main challenge that web component design systems with scoping have had with consumers in micro-front-end style apps.

Also since React 19 seems a bit delayed while the team irons out details with concurrent data fetches in RSCs, I wonder if there's still time to maybe get this into React 19?

michaelwarren1106 commented 3 months ago

not stale :)

Is there a possibility of discussing this and getting it addressed before react 19 ships? The inability to tell react that its root is inside a shadow root is a definite blocker for micro-frontend applications where each remote app has web components in it that need scoping.

github-actions[bot] commented 2 days ago

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

michaelwarren1106 commented 2 days ago

definitely not stale :)

the scoped element registry implementations are hitting browser nightlies and such and are getting stable. would be awesome to see some solution for react!