Use events with web components and react

My react app needs to load dynamic html at runtime. Inside of this html, I need to display small custom components.

At first I tried to render a separate react virtual dom for each of these custom components. But I think it might be cleaner to do this with web components. The components don’t need to react to the app state, I can just add <my-component> tags to the dynamic html.

While the components don’t react to app state, they do need to communicate “user has interacted with me” to the rest of the app. It’s a one-way information flow, out of the dynamic html.
It seems like there are two ways to implement this:

  • use querySelectors to find all the web components and give them a callback as a prop.
  • dispatch a custom “user interaction” event

For now, I’m trying to make this second option, with custom events, work. And this is where I run into problems.
It’s not clear to me how to listen to custom events from components inside of the react dom. The react docs do say:

Events emitted by a Web Component may not properly propagate through a React render tree. You will need to manually attach event handlers to handle these events within your React components.

But even when I manually attach event handlers, they don’t react to custom events.

To reproduce the problem, I’ve set up a minimal example. There are two divs in the dom, the outer one listens to custom events, the inner one hosts the react virtual dom. Inside of the virtual dom there are two components. The outer one is also a listener, the inner one dispatches the custom event.

The problem is: Only the outer div catches the event, react components can’t listen to it.

You can see the example here: https://codesandbox.io/s/quirky-cannon-in5u0

Or you can clone and edit a typescript version:

git clone https://github.com/lhk/react_custom_events
cd react_custom_events
npm i
npm run start
# browser opens, look at the console output

After quite some experimentation, it seems to me that this is simply not possible with react.

The synthetic events implemented by react introduce a series of limitations:

  • there are no synthetic versions of custom events
  • native (custom) events will bubble through the virtual dom, without triggering any event listeners. After leaving the virtual dom, they will continue to bubble through the remainder of the dom. This is what happens in my example code.
  • it is not possible to dispatch synthetic events yourself

It is possible to listen to custom events in the virtual dom, but only on the same element that dispatches them. The following code will catch the event:

export default function Dispatcher() {
  const pRef = useRef(null);
  useEffect(() => {
    (pRef.current as any).addEventListener('custom', ()=>{
      console.log('react caught custom event directly on component');
    });
    (pRef.current as any).dispatchEvent(customEvent);
  });
  return (
    <div>
      <p ref={pRef}>Some Text</p>
    </div>
  );
}

There are github issues discussing custom events in react. But the facebook team seems to moderate this topic quite heavily:

I would argue that this makes custom events pretty much useless. If you have to get a reference to the specific dom node that triggered the custom event, you might as well give it a callback.
So that’s what I’ll try next: look up all the web components and connect them to the virtual dom via javascript callbacks.

This seems to be a key advantage of preact: It does not use synthetic events. In preact, every event is just a native dom event. This makes for much better support of custom components.

By the way, this is also the reason why it’s not possible to render a react virtual dom inside of a shadow dom:

The problem is that React has a global event handler on the document and the shadow DOM retargets the event to make it look like it came from the host node. This prevents React from passing the event to the right element.

Nice SO post on this: