Search code examples
reactjsdomweb-component

Is there a counterpart to Angular "slots" for DOM nodes in React?


The goal is to create a W3C web component in React which supports arbitrary DOM nodes as children.

The initial markup in the browser should be like this:

  <custom-button>
    some <u>styled</u> text here
  </custom-button>

I would then:

  • call customElements.define() to register my React code as the implementation of the custom-button,
  • inside the implementation create a shadow root inside <custom-button>,
  • afterwards call ReactDOM.render(<CustomButton ...>, shadowRoot); to populate this shadow root

The DOM structure in the browser is now:

  <custom-button>
    #shadow-root
      <div class="CustomButton">
        <!-- x -->
      </div>
    some <u>styled</u> text here
  </custom-button>

But this is not really the desired results; I need the original content of <custom-button> to render inside <div class="CustomButton"> now.

I am aware of the React children prop, but as I understand it, it will only work for children that were also declared inside the React implementation and not with arbitrary DOM nodes that were created on the surrounding web component element.

On the other hand I read that Angular implements a concept they call "transclusion" in which they provide slots which DOM children web component will be mapped into. Is there anything similar in React?

One quick "workaround" that I could think of in React is to:

  • obtain the ref of the top-level JSX tag,
  • find the shadow root in ref.parentNode
  • iterate through all children and re-attach them to ref as new parent

This would only cover the initialization though. If, at runtime, some other script tried to append more children to <custom-button>, or re-order or delete previously inserted children, this would probably fail.


Solution

  • I have just realized that the <slot /> of the web components standard solves the problem.

    So, to reflect the content of the <custom-element> inside the React component, the following is completely sufficient:

      render() {
        return (
          <div className="customElement">
            ...
            <slot />
            ...
          </div>
        );
      }