Click event not firing when React Component in a Shadow DOM

react shadow dom
npm react shadow dom retarget events
web components events
shadowroot addeventlistener
react shadow styled components
react mouse events
react #shadow root closed
shadow dom events

I have a special case where I need to encapsulate a React Component with a Web Component. The setup seems very straight forward. Here is the React Code:

// React Component
class Box extends React.Component {
  handleClick() {
    alert("Click Works");
  }
  render() {
    return (
      <div 
        style={{background:'red', margin: 10, width: 200, cursor: 'pointer'}} 
        onClick={e => this.handleClick(e)}>

        {this.props.label} <br /> CLICK ME

      </div>
    );
  }
};

// Render React directly
ReactDOM.render(
  <Box label="React Direct" />,
  document.getElementById('mountReact')
);

HTML:

<div id="mountReact"></div>

This mounts fine and the click event works. Now when I created a Web Component wrapper around the React Component, it renders correctly but the click event doesn't work. Here is my Web Component Wrapper:

// Web Component Wrapper
class BoxWebComponentWrapper extends HTMLElement {
  createdCallback() {
    this.el      = this.createShadowRoot();
    this.mountEl = document.createElement('div');
    this.el.appendChild(this.mountEl);

    document.onreadystatechange = () => {
      if (document.readyState === "complete") {
        ReactDOM.render(
          <Box label="Web Comp" />,
          this.mountEl
        );
      }
    };
  }
}

// Register Web Component
document.registerElement('box-webcomp', {
  prototype: BoxWebComponentWrapper.prototype
});

And here is the HTML:

<box-webcomp></box-webcomp>

Is there something I'm missing? Or does React refuse to work inside a Web Component? I have seen a library like Maple.JS which does this sort of thing, but their library works. I feel like I'm missing one small thing.

Here is the CodePen so you can see the problem:

http://codepen.io/homeslicesolutions/pen/jrrpLP

As it turns out the Shadow DOM retargets click events and encapsulates the events in the shadow. React does not like this because they do not support Shadow DOM natively, so the event delegation is off and events are not being fired.

What I decided to do was to rebind the event to the actual shadow container which is technically "in the light". I track the event's bubbling up using event.path and fire all the React event handlers within context up to the shadow container.

I added a 'retargetEvents' method which binds all the possible event types to the container. It then will dispatch the correct React event by finding the "__reactInternalInstances" and seek out the respective event handler within the event scope/path.

retargetEvents() {
    let events = ["onClick", "onContextMenu", "onDoubleClick", "onDrag", "onDragEnd", 
      "onDragEnter", "onDragExit", "onDragLeave", "onDragOver", "onDragStart", "onDrop", 
      "onMouseDown", "onMouseEnter", "onMouseLeave","onMouseMove", "onMouseOut", 
      "onMouseOver", "onMouseUp"];

    function dispatchEvent(event, eventType, itemProps) {
      if (itemProps[eventType]) {
        itemProps[eventType](event);
      } else if (itemProps.children && itemProps.children.forEach) {
        itemProps.children.forEach(child => {
          child.props && dispatchEvent(event, eventType, child.props);
        })
      }
    }

    // Compatible with v0.14 & 15
    function findReactInternal(item) {
      let instance;
      for (let key in item) {
        if (item.hasOwnProperty(key) && ~key.indexOf('_reactInternal')) {
          instance = item[key];
          break;
        } 
      }
      return instance;
    }

    events.forEach(eventType => {
      let transformedEventType = eventType.replace(/^on/, '').toLowerCase();

      this.el.addEventListener(transformedEventType, event => {
        for (let i in event.path) {
          let item = event.path[i];

          let internalComponent = findReactInternal(item);
          if (internalComponent
              && internalComponent._currentElement 
              && internalComponent._currentElement.props
          ) {
            dispatchEvent(event, eventType, internalComponent._currentElement.props);
          }

          if (item == this.el) break;
        }

      });
    });
  }

I would execute the "retargetEvents" when I render the React component into the shadow DOM

createdCallback() {
    this.el      = this.createShadowRoot();
    this.mountEl = document.createElement('div');
    this.el.appendChild(this.mountEl);

    document.onreadystatechange = () => {
      if (document.readyState === "complete") {

        ReactDOM.render(
          <Box label="Web Comp" />,
          this.mountEl
        );

        this.retargetEvents();
      }
    };
  }

I hope this works for future versions of React. Here is the codePen of it working:

http://codepen.io/homeslicesolutions/pen/ZOpbWb

Thanks to @mrlew for the link which gave me the clue to how to fix this and also thanks to @Wildhoney for thinking on the same wavelengths as me =).

Events not registered inside shadow dom � Issue #10422 � facebook , A React Component with an Event Handler (for example onClick) is Click events/HTML injection not working correctly in Shadow DOM? When you render a react component inside a shadow DOM, events will not be dispatched in react. I.e. you do something like this: < div onClick = {() => alert (' I have been clicked ')} > foo bar < / div >

I fixed a bug cleaned up the code of @josephvnu's accepted answer. I published it as an npm package here: https://www.npmjs.com/package/react-shadow-dom-retarget-events

Usage goes as follows

Install

yarn add react-shadow-dom-retarget-events or

npm install react-shadow-dom-retarget-events --save

Use

import retargetEvents and call it on the shadowDom

import retargetEvents from 'react-shadow-dom-retarget-events';

class App extends React.Component {
  render() {
    return (
        <div onClick={() => alert('I have been clicked')}>Click me</div>
    );
  }
}

const proto = Object.create(HTMLElement.prototype, {
  attachedCallback: {
    value: function() {
      const mountPoint = document.createElement('span');
      const shadowRoot = this.createShadowRoot();
      shadowRoot.appendChild(mountPoint);
      ReactDOM.render(<App/>, mountPoint);
      retargetEvents(shadowRoot);
    }
  }
});
document.registerElement('my-custom-element', {prototype: proto});

For reference, this is the full sourcecode of the fix https://github.com/LukasBombach/react-shadow-dom-retarget-events/blob/master/index.js

Event Handler on React Component not invoked when React , Let's say, a click event happens inside a shadow DOM of Retargeting does not occur if the event occurs on a slotted element, that physically� The idea behind shadow tree is to encapsulate internal implementation details of a component. Let’s say, a click event happens inside a shadow DOM of <user-card> component. But scripts in the main document have no idea about the shadow DOM internals, especially if the component comes from a 3rd-party library.

Replacing this.el = this.createShadowRoot(); with this.el = document.getElementById("mountReact"); just worked. Maybe because react has a global event handler and shadow dom implies event retargeting.

Shadow DOM and events, Shadow DOM allows web developers to create compartmentalized DOM Therefore, it brings solutions for common problems in web Composition is why native elements like <select> , <details> , <form> This is a great way for your component to encapsulate behaviors that react to user interaction or� In React, the onClick handler allows you to call a function and perform an action when an element is clicked. onClick is the cornerstone of any React app. Click on any of the examples below to see code snippets and common uses: Call a Function After Clicking a Button; Call an Inline Function in an onClick Event Handler; Call Multiple Functions

I've discovered another solution by accident. Use preact-compat instead of react. Seems to work fine in a ShadowDOM; Preact must bind to events differently?

Click+event+not+firing+when+React+Component+in+a+Shadow+ , An important aspect of web components is encapsulation — being able to keep page so that different parts do not clash, and the code can be kept nice and clean . The Shadow DOM API is a key part of this, providing a way to attach file we define a class called PopUpInfo , which extends HTMLElement : There is a known issue with certain events not firing for React when using Shadow DOM. One solution to the problem is to install the react-shadow-dom-retarget-events package.

Shadow DOM v1: Self-Contained Web Components, This does not happen immediately, but it is scheduled to happen later as needed. When the shadow DOM renderer needs to render the visual tree, these internal If the user clicks on the div the real target of the click event is the <b> element. bad things happening before the callback has a chance to react to changes. There are issues with events not working as expected with React in the Shadow DOM, however the react-shadow package takes care of those issues and I've verified that the events are being dispatched correctly for other components and DOM elements - however with react-shadow (and other mention-based packages as well) the onChange event is not seen.

Using shadow DOM, Working with the Shadow DOM Jason Strimpel The shadow DOM is not the dark These shadow DOM subtrees can be associated with an element, but do not <p class= "response" ></p> <script type= "text/javascript" > ( function () { var p The mechanism for extracting content from the shadow host is the select attribute. React defines these synthetic events according to the W3C spec, so you don’t need to worry about cross-browser compatibility. See the SyntheticEvent reference guide to learn more. When using React, you generally don’t need to call addEventListener to add listeners to a DOM element after it is created.

Polyfills, React to input 5. Theming with custom The host has a property called shadowRoot that refers to the shadow root. The shadow root has a The callback argument lists added and removed nodes, not just elements. If you're only If the user clicks on the image element the click event bubbles up the tree: A listener on the� react-shadow-dom-retarget-events What it does. Fixes events for react components rendered in a shadow dom. Why. When you render a react component inside shadow dom events will not be dispatched to react. I.e. when a user clicks in your react component nothing happens. This happens (or does not happen) with any events. A bug is filed at #10422

Comments
  • What is this special case you speak of? is it for experimentation only?
  • @Seth Just a Proof of concept to see if we can encapsulate a React component in a Web Component so we can use it in some of our applications that doesn't have React as the main framework. It may be a far fetch approach, but just want to see if it's possible.
  • Great solution. Wonder why react team do not want to integrate it. I saw two PR about this.(
  • Will you "open-source" it? I found some bug (lots of dispatchEvent calls) and it will be nice to fix it and share.
  • Oh man wanna give you 10.000 upvotes on this. You saved my life.
  • @Dimitry and josephnvu I fixed a bug, cleaned up the code and published this workaround at npmjs.com/package/react-shadow-dom-retarget-events
  • @Lukas it was so long time ago, I do not remember.( That's the script I used gist.github.com/DimitryDushkin/c091d5a6c33e10641eef0828261d5398 but seems like there is a bug as awwester said.
  • But i want to use the shadow dom. Otherwise it's not encapsulated. The challenge here is to have a React component Wrapped in a custom element.
  • I found this. Maybe worth checking out.
  • thanks @mrlew. This part helps: "As Shadow DOM has the concept of Event Retargeting for encapsulation purposes, event delegation will not function correctly because all events will appear to be coming from the Shadow DOM – therefore ReactShadow uses the React ID for each element to dispatch the event from the original element, therefore maintaining React's event delegation implementation."
  • Figured out a solution. See above.