React: createPortal: support option to stop propagation of events in React tree

Created on 27 Oct 2017  ·  103Comments  ·  Source: facebook/react

Do you want to request a feature or report a bug?
Feature, but also a bug cause new API breaks old unstable_rendersubtreeintocontainer

What is the current behavior?
We cannot stop all events propagation from portal to its React tree ancestors. Our layers mechanism with modals/popovers completely broken. For example, we have a dropdown button. When we click on it, click opens popover. We also want to close this popover when clicking on same button. With createPortal, click inside popover fires click on button, and it's closing. We can use stopPropagation in this simple case. But we have tons of such cases, and we need use stopPropagation for all of them. Also, we cannot stop all events.

What is the expected behavior?
createPortal should have an option to stop synthetic events propagation through React tree without manually stopping every event. What do you think?

DOM Feature Request

Most helpful comment

Even that seems needlessly complex to me. Why not simply add an optional boolean flag to createPortal allowing the bubbling behavior to be blocked?

All 103 comments

Also, propagation of mouseOver/Leave looks completely unexpected.
image

Can you move portal outside of the button?

e.g.

return [
  <div key="main">
    <p>Hello! This is first step.</p>
    <Button key="button" />
  </div>,
  <Portal key="portal" />
];

Then it won't bubble through the button.

It was my first thought, but!) Imagine, that we have mouseEnter handler in such component container:

image

With unstable_rendersubtreeintocontainer i need nothing to do with events in ButtonWithPopover component – mouseEnter simply works when mouse really enters div and button DOM element, and not fired when mouse is over popover. With portal, event fires when mouse over popover – and actually NOT over div in this moment. So, i need to stop propagation. If i do it in ButtonWithPopover component, i will break event firing when mouse is over button. If i do it in popover and i'm using some common popover component for this application, i also can break logic in other app parts.

I really don't understand purpose of bubbling through React tree. If i need events from portal component – i simply can pass handlers through props. We were do it with unstable_rendersubtreeintocontainer and it worked perfect.

If i will open a modal window from some button deep in react tree, i will receive unexpected firing of events under modal. stopPropagation will also stop propagation in DOM, and i will not get events that i really expect to be fired(

@gaearon I would suggest this is more of a bug than a feature request. We have a number of new bugs caused by mouse events bubbling up through portals (where we were previously using unstable_rendersubtreeintocontainer). Some of these can't be fixed even with an extra div layer to filter mouse events because e.g. we rely on mousemove events propagating up to the document to implement draggable dialogs.

Is there a way to workaround this before this is addressed in a future release?

I think it's being called a feature request, because the current bubble behavior of portals is both expected and intended. The goal is that subtree act like real child of their parents.

What would be helpful is additional use cases or situations (like the ones you're seeing) that you don't feel are served by the current implementation, or are difficult to workaround

I understand that this behavior is intended, but I think it's a significant bug that it's not disable-able.

In my mind library working with DOM should preserve DOM implementation behavior not break it.

For example:

class Container extends React.Component {
  shouldComponentUpdate = () => false;
  render = () => (
    <div
      ref={this.props.containerRef}
      // Event propagation on this element not working
      onMouseEnter={() => { console.log('handle mouse enter'); }}
      onClick={() => { console.log('handle click'); }}
    />
  )
}

class Root extends React.PureComponent {
  state = { container: null };
  handleContainer = (container) => { this.setState({ container }); }

  render = () => (
    <div>
      <div
        // Event propagation on this element not working also
        onMouseEnter={() => { console.log('handle mouse enter'); }}
        onClick={() => { console.log('handle click'); }}
      >
        <Container containerRef={this.handleContainer} />
      </div>
      {this.state.container && ReactDOM.createPortal(
        <div>Portal</div>,
        this.state.container
      )}
    </div>
  );
}

When I work with DOM, I expect to receive events like DOM implementation does it. In my example events are propagated through Portal, working around it's DOM parents, and this can be considered as a bug.

Folks thanks for the discussion, however I don't think it's all that helpful to argue whether something is a bug or not. Instead i'd be more productive to discuss the use cases and examples that are not met by the current behavior, so we can better understand if the current way is the best way for the future.

In general we want the API to handle a diverse set of use-cases while hopefully not overly limiting others. I can't speak for the core team, but I'd imagine that making it configurable is not a likely solution. Generally React leans for a consistent api over configurable ones.

I also understand that this behavior is not how the DOM works, but i don't think that's in itself a good reason to say it shouldn't be that way. Lots of react-dom's behavior is different from how the DOM works, may events are already different from the native version. onChange for instance is completely unlike the native change event, and all react events bubble regardless of type, unlike the DOM.

Instead i'd be more productive to discuss the use cases and examples that are not met by the current behavior

Here's two examples that are broken for us in our migration to React 16.

First, we have a draggable dialog which is launched by a button. I attempted to add a "filtering" element on our Portal use which called StopPropagation on any mouse* an key* events. However, we rely on being able to bind a mousemove event to the document in order to implement the dragging functionality -- this is common because if the user moves the mouse at any significant rate, the cursor leaves the bounds of the dialog and you need to be able to capture the mouse movement at a higher level. Filtering these events breaks this functionality. But with Portals, the mouse and key events are bubbling up from inside the dialog to the button that launched it, causing it to display different visual effects and even dismiss the dialog. I don't think it's realistic to expect every component that will be launched via a Portal to bind 10-20 event handlers to stop this event propagation.

Second, we have a popup context menu which can be launched by either a primary- or secondary-mouse click. One of the internal consumers of our library has mouse handlers attached to the element that launches this menu, and of course the menu also has click handlers for handling item selection. The menu is now reappearing on every click as the mousedown/mousedown events are bubbling back up to the button that launches the menu.

I can't speak for the core team, but I'd imagine that making it configurable is not a likely solution. Generally React leans for a consistent api over configurable ones.

I implore you (and the team) to reconsider this position in this particular case. I think event bubbling will be interesting for certain use cases (although I can't think of any offhand). But I think it will be crippling in others, and it introduces significant inconsistency in the API. While unstable_rendersubtreeintocontainer was never super-supported, it was what everyone used to render outside of the immediate tree, and it didn't work this way. It was officially deprecated in favor of Portals, but Portals break the functionality in this critical way, and there doesn't seem to be an easy workaround. I think this can be fairly described as quite inconsistent.

I also understand that this behavior is not how the DOM works, but i don't think that's in itself a good reason to say it shouldn't be that way.

I understand where you're coming from here, but I think in this case (a) it's a fundamental behavior which (b) currently has no workaround, so I think "the DOM doesn't work this way" is a strong argument, if not a completely convincing one.

And to be clear: my request that this be considered a bug is mostly so that it gets prioritized for a fix sooner rather than later.

My mental model of a Portal is that it behaves as if it is on the same place in the tree, but avoids problems such as "overflow: hidden" and avoids scrolling for drawing/layout purposes.

There are many similar "popup" solutions that happen inline without a Portal. E.g. a button that expands a box right next to it.

Take as an example the "Pick your reaction" dialog here on GitHub. That is implemented as a div right next to the button. That works fine now. However, if it wants to have a different z-index, or be lifted out of an overflow: scroll area that contains these comments, then it will need to change DOM position. That change is not safe to do unless other things like event bubbling is also preserved.

Both styles of "popups" or "pop outs" are legit. So how would you solve the same problem when the component is inline in the layout as opposed to floating outside of it?

The workaround that worked for me is calling stopPropagation directly under portal rendering:

return createPortal(
      <div onClick={e => e.stopPropagation()}>{this.props.children}</div>,
      this.el
    )

That works great for me since I have single abstraction component that uses portals, otherwise you will need to fix up all your createPortal calls.

@methyl this assumes you know every event that you need to block from bubbling up the tree. And in the case I mentioned with draggable dialogs, we need mousemove to bubble up to document, but not to bubble up the render tree.

Both styles of "popups" or "pop outs" are legit. So how would you solve the same problem when the component is inline in the layout as opposed to floating outside of it?

@sebmarkbage I'm not sure this question makes sense. If I had this problem inlining the component, I wouldn't inline it.

I think some of problem here is the some use cases of renderSubtreeIntoContainer are being ported to createPortal when the two methods are doing conceptually different things. The concept of Portal was being overloaded I think.

I agree that in the Modal dialog case, you almost never want the modal to act like a child of the button that opened it. The trigger component is only rendering it because it controls the open state. I think tho it's a mistake to say that the portal implementation is therefore wrong, instead of saying that createPortal in the button is not the right tool for this. In this case the Modal is not a child of the trigger, and shouldn't be rendered as if it were. One possible solution is to keep using renderSubtreeIntoContainer, another user-land option is to have a ModalProvider near the app root that handles rendering modals, and passes down (via context) a method to render an arbitrary modal element need to the root

renderSubtreeIntoContainer can't be called from inside of render or lifecycle methods in React 16, which pretty much precludes its use for the cases I've been discussing (in fact, all our components which were doing this completely broke in the migration to 16). Portals are the official recommendation: https://reactjs.org/blog/2017/09/26/react-v16.0.html#breaking-changes

I agree that the concept of Portals might have ended up overloaded. Not sure I love the solution of a global component and context for it, though. It seems like this could be easily solved by a flag on createPortal specifying whether events should bubble through. It would be an opt-in flag which would preserve API compatibility with 16+.

I will try to clarify our use-case of the portals and why we would love to see an option for stopping events propagation. In ManyChat app, we are using portals to create a 'layers'. We have the layer system for the whole app that used by several types of components: popovers, dropdowns, menus, modals. Every layer can expose a new layer, e.g. button on a second level of menu can trigger the modal window where the other button can open the popover. In most cases layer is the new branch of UX that solves it's own task. And when new layer is open, the user should interact with this new layer, not with others in bottom. So, for this system, we've created a common component for rendering to layer:

class RenderToLayer extends Component {
  ...
  stop = e => e.stopPropagation()

  render() {
    const { open, layerClassName, useLayerForClickAway, render: renderLayer } = this.props

    if (!open) { return null }

    return createPortal(
      <div
        ref={this.handleLayer}
        style={useLayerForClickAway ? clickAwayStyle : null}
        className={layerClassName}
        onClick={this.stop}
        onContextMenu={this.stop}
        onDoubleClick={this.stop}
        onDrag={this.stop}
        onDragEnd={this.stop}
        onDragEnter={this.stop}
        onDragExit={this.stop}
        onDragLeave={this.stop}
        onDragOver={this.stop}
        onDragStart={this.stop}
        onDrop={this.stop}
        onMouseDown={this.stop}
        onMouseEnter={this.stop}
        onMouseLeave={this.stop}
        onMouseMove={this.stop}
        onMouseOver={this.stop}
        onMouseOut={this.stop}
        onMouseUp={this.stop}

        onKeyDown={this.stop}
        onKeyPress={this.stop}
        onKeyUp={this.stop}

        onFocus={this.stop}
        onBlur={this.stop}

        onChange={this.stop}
        onInput={this.stop}
        onInvalid={this.stop}
        onSubmit={this.stop}
      >
        {renderLayer()}
      </div>, document.body)
  }
  ...
}

This component stops propagation for all event types from React docs, and it allowed us to update to React 16.

Does this need to be tied to portals? Rather than sandboxing portals, what if there was just a (for example) <React.Sandbox>...</React.Sandbox>?

Even that seems needlessly complex to me. Why not simply add an optional boolean flag to createPortal allowing the bubbling behavior to be blocked?

@gaearon this is a pretty unfortunate situation for a certain slice of us -- could you or someone dear to you have a look at this? :)

I'd add that my current thinking is that both use cases should be supported. There are really use cases where you need context to flow from the current parent to the subtree but to not have that subtree act as a logical child in terms of the DOM. Complex modals are the best example, you just almost never want the events from a form in a modal window to propagate up to the trigger button, but almost certainly need the context passed through (i18n, themes, etc)

I will say that that usecase _could_ be mostly solved with a ModalProvider closer to the app root that renders via createPortal high enough that event propagation doesn't affect anything, but that starts to feel like a workaround as opposed to a well designed architecture. It also makes library provided modals more annoying for users since they are no longer self contained.

i would add tho in terms of API i don't think createPortal should do both, the modal case really wants to just use ReactDOM.render (old skool) because it's pretty close to a distinct tree _except_ that context propagation is often needed

We just had to fix an extremely difficult-to-diagnose bug in our outer application's focus management code as a result of using the workaround that @kib357 posted.

Specifically: calling stopPropagation on the synthetic focus event to prevent it from bubbling out of the portal causes stopPropagation to also be called on the native focus event in React's captured handler on #document, which meant it did not make it to another captured handler on <body>. We fixed by moving our handler up to #document, but we had specifically avoided doing that in the past so as not to step on React's toes.

The new bubbling behavior in Portals really feels like the minority case to me. Be that opinion or truth, could we please get some traction on this issue? Maybe @gaearon? It's four months old and causing real pain. I think this could fairly be described as a bug given that it's a breaking API change in React 16 with no completely-safe workaround.

@craigkovatch I'm still curious how you would solve my inline example. Let's say the popup is pushing the size of the box down. Inlining something is important since it is pushing something down in the layout given its size. It can't just hover over.

You could potentially measure the popover, insert a blank placeholder with the same size and try to align it on top but that's not what people do.

So if your popover need to expand the content in place, like right next to the button, how would you solve it? I suspect that the pattern that works there, will work in both cases and we should just recommend the one pattern.

I think in general this is the pattern that works in both scenarios:

class Foo extends React.Component {
  state = {
    highlight: false,
    showFlyout: false,
  };

  mouseEnter() {
    this.setState({ highlight: true });
  }

  mouseLeave() {
    this.setState({ highlight: false });
  }

  showFlyout() {
    this.setState({ showFlyout: true });
  }

  hideFlyout() {
    this.setState({ showFlyout: false });
  }

  render() {
    return <>
      <div onMouseEnter={this.mouseEnter} onMouseLeave={this.mouseLeave} className={this.state.highlight ? 'highlight' : null}>
        Hello
        <Button onClick={this.showFlyout} />
      </div>
      {this.state.showFlyout ? <Flyout onHide={this.hideFlyout} /> : null}
    </>;
  }
}

If the Flyout is a portal, then it works and it doesn't get any mouse over events when hovering over the portal. But more importantly, it also works if it is NOT a portal, and it needs to be an inline flyout. No stopPropagation needed.

So what is it about this pattern that doesn't work for your use case?

@sebmarkbage we are using Portals in a completely different fashion, rendering into a container mounted as the final child of <body> which is then positioned, sometimes with a z-index. The React documentation suggests this is closer to the design intention; i.e. rendering into a totally different place in the DOM. It doesn't seem to me that our use cases are similar enough for discussion to belong on this thread. But if you want to brainstorm/troubleshoot together, I'd be more than happy to discuss further in another forum.

No my use case is both. Sometimes one and sometimes the other. That's why it is relevant.

The <Flyout /> can choose to render into the final child of body or not but as long as you hoist the portal itself out to a sibling of the hovered component rather than a child of it, your scenario works.

I think there's a plausible scenario where that is inconvenient and you want a way to teleport things from deeply nested components but in that scenario you're probably fine with the context being the context from the intermediate point. But I think of those as two separate issues.

Maybe we need a slots API for that.

class Foo extends React.Component {
  state = {
    showFlyout: false,
  };

  showFlyout() {
    this.setState({ showFlyout: true });
  }

  hideFlyout() {
    this.setState({ showFlyout: false });
  }

  render() {
    return <>
      Hello
      <Button onClick={this.showFlyout} />
      <SlotContent name="flyout">
        {this.state.showFlyout ? <Flyout onHide={this.hideFlyout} /> : null}
      </SlotContent>
    </>;
  }
}

class Bar extends React.Component {
  state = {
    highlight: false,
  };

  mouseEnter() {
    this.setState({ highlight: true });
  }

  mouseLeave() {
    this.setState({ highlight: false });
  }

  render() {
    return <>
      <div onMouseEnter={this.mouseEnter} onMouseLeave={this.mouseLeave} className={this.state.highlight ? 'highlight' : null}>
        <SomeContext>
          <DeepComponent />
        </SomeContext>
      </div>
      <Slot name="flyout" />
    </>;
  }
}

The portal would then get the context of Bar, not DeepComponent. Context and event bubbling then still share the same tree path.

@sebmarkbage the modal case usually does require context from the point it's rendered. It's slightly unique of a case I think, the component is a logical child of the thing that rendered it but _not_ a structural one (for lack of a better word), e.g. You usually want things like form context (relay, formik, redux form, whatever) but not DOM events to pass through. One also ends up rendering such modals fairly deep in trees, next to their triggers, so they stay componenty and reusable, more so than because they belong there structurally

I think this case is generally different to the flyout/dropdown case createPortal serves. Tbc I think the bubbling behavior of portals is good, but not for modals. I also think this could handled with Context and some sort of ModalProvider reasonably well, but that's kinda annoying especially for libraries.

as long as you hoist the portal itself out to a sibling of the hovered component rather than a child of it, your scenario works.

Not sure I follow. There's still the problem of e.g. keyDown events bubbling through an unexpected DOM tree.

@jquense Note that in my example the slot is still within the Bar component so it would get its context from the form in something like <Form><Bar /></Form>.

Even if the portal rendered into the document body.

So it’s like two indirections (portalings): deep -> sibling of Bar -> document body.

So the context of the portal is still the context of the form, and so is the event bubbling chain, but neither is in the context of the hovered thing.

Yes sorry missed that 😳 If I'm reading it right tho, you'd still have the bubbling at <Slot> up though? That's definitely better, though I do think that in the Modal dialog case one probably doesn't want _any_ bubbling. Like thinking in terms of a screen reader, you want everything outside the modal to be invert, while it's up. I don't know, I think for that case the bubbling is a gotcha, no one would expect a click inside a dialog to bubble through anywhere.

Maybe the problem here isn't portals, but there isn't a good way to share Context across trees? A part from the context thing ReactDOM.render is really fine for modals, and maybe more "correct" way of thinking about it anyway...

My thinking here is that there is some bubbling because it still goes from the modal to the div to the body to the document to the window. And conceptually beyond the frame out to the containing window and so on.

That's not theoretical in something like ART or GL rendered content (and to some extent React Native) where there might not be an existing backing tree to get those semantics from. So there needs to be a way to say that this is where it bubbles.

In some apps there are modals in modals. E.g. at FB there is chat window that might be above a modal or a modal might be part of the chat window. So even a modal has some context as to where in the tree it belongs. It is never completely standalone.

That's not to say we can't have two different semantics for event bubbling and context. That are both explicit about this and you can portal one without the other etc.

Having the guarantee that they both follow the same path is really powerful though since it means that event bubbling can be fully implemented for user space events the same as in the browser.

For example this happens with various Redux contexts today. Imagine this.context.dispatch("Hover") is a user space event bubbling. We could even implement React events as part of context. It seams reasonable to think that I can use this the same way, and in every way right now, you can. I think if we did fork these two contexts, we'd probably end up with another user space context API that follows the DOM structure in parallel with normal context - if they are truly so different.

So that's kind of why I'm pushing against it a bit to see if the slots thing might be sufficient, since a) you need to be explicit about which context bubbling happens anyway. b) it can avoid forking the world and having two entire context systems.

Specifically: calling stopPropagation on the synthetic focus event to prevent it from bubbling out of the portal causes stopPropagation to also be called on the native focus event in React's captured handler on #document, which meant it did not make it to another captured handler on . We fixed by moving our handler up to #document, but we had specifically avoided doing that in the past so as not to step on React's toes.

@craigkovatch , did you used onFocusCapture event on document? In my workaround captured events shouldn't be stopped. Can you provide more detailed example of how it was and what you have done to resolve your issue?
Also, i think my code has an issue with stopping blur event – it shouldn't be stopped. So, i'll investigate this question deeper and will try to found a more reliable solution.

@kib357 I'm not suggesting there's an issue in your workaround, I think there's a separate bug in React there (i.e. shouldn't cancel propagation of native focus events in capture phase when calling stopPropagation on synthetic focus events in bubbling phase).

The code in question uses a native capture event listener, i.e. document.body.addEventListener('focus', handler, true)

@craigkovatch sounds interesting, given the fact that you used captured handler. However, i have not any thoughts why this happens.

So, guys, we have a two different scenarios for using portal rendering:

  1. To prevent CSS issues like overflow:hidden and etc in simple widgets, like a dropdown buttons or one-level menus
  2. To create a new UX layer for more powerful cases like:
  3. modals
  4. nested menus
  5. popovers-with-forms-with-dropdowns-... – all cases, when layers are combined

I think current createPortal API satisfies only the first scenario. Suggestion to use a new React.render for second is unusable – it's very poor to create a separate app with all it providers for every layer.
What additional info we can provide to help resolve this issue?
What disadvantages of suggested param in the createPortal API?

@sebmarkbage My immediate question with the slots API is: would I be able to insert multiple SlotContents into one Slot at the same time? It's not uncommon in our interface to have multiple "popups" or "modals" open simultaneously. In my perfect world a Popup API would look something like this:

import { App } from './app'
import { PopupSlot } from './popups'

let root = (
  <div>
    <App />
    <PopupSlot />
  </div>
)

ReactDOM.render(root, document.querySelector('#root'))

// some dark corner of our app

import { Popup } from './popups'

export function SoManyPopups () {
  return <>
    <Popup>My Entire</Popup>
    <Popup>Interface</Popup>
    <Popup>Is Popups</Popup>
  </>
}

We have a new issue with this that I have been completely unable to find a workaround for. Using the "event trap" approach suggested above, only React Synthetic events are blocked from bubbling out of the portal. The native events still bubble, and since our React code is hosted inside of a mostly-jQuery application, the global jQuery keyDown handler on <body> still gets the event.

I attempted to add an event.stopPropagation listener to the native container element inside the Portal via a ref like this, but this completely neuters all Synthetic events within the portal -- I incorrectly assumed React's top-level listener was watching capture phase.

Not sure what can possibly be done here, other than changes to React.

const allTheEvents: string[] = 'click contextmenu doubleclick drag dragend dragenter dragexit dragleave dragover dragstart drop mousedown mouseenter mouseleave mousemove mouseover mouseout mouseup keydown keypress keyup focus blur change input invalid submit'.split(' ');
const stop = (e: React.SyntheticEvent<HTMLElement>): void => { e.stopPropagation(); };
const nativeStop = (e: Event): void => e.stopPropagation();
const handleRef = (ref: HTMLDivElement | null): void => {
  if (!ref) { return; }
  allTheEvents.forEach(eventName => ref.addEventListener(eventName, nativeStop));
};


/** Prevents https://reactjs.org/docs/portals.html#event-bubbling-through-portals */
export function PortalEventTrap(children: React.ReactNode): JSX.Element {
  return <div
      onClick={stop}
      ...

      ref={handleRef}
    >
      {children}
    </div>;
}

That depends on the order that ReactDOM and JQuery are initialized. If JQuery initializes first, JQuery's top level event handlers will be installed first, and so they will run before ReactDOM's synthetic handlers get to run.

Both ReactDOM and JQuery prefer to only have a single top level listener that then simulates bubbling internally, unless there's some event that the browser won't bubble such as scroll.

@Kovensky my understanding was that jQuery did not do "synthetic bubbling" the way React does, and thus does not have a single top-level listener. My DOM inspector doesn't reveal one, either. Would love to see what you're referencing if I'm mistaken.

That will be the case for delegated events. For example, $(document.body).on('click', '.my-selector', e => e.stopPropagation()).

Look, this can be solved in React, if someone just convinces me that this can’t be solved with my proposed design above which requires some restructuring of your code. But I haven’t seen any reason that can’t be done other than just trying to find a quick fix workaround.

@sebmarkbage your proposal only solves the case of the events propagating to the immediate owner. What about the rest of the tree?

Here is a use case I think can't be solved well with Slots or createPortal

<Form defaultValue={fromValue}>
   <more-fancy-markup />
   <div>
     <Field name="faz"/>
     <ComplexFieldModal>
       <Field name="foo.bar"/>
       <Field name="foo.baz"/>
     </ComplexFieldModal>
  </div>
</Form>

And here is a gif with a similar but slightly different setup, where i'm using createPortal for a responsive site, to move a form field to the app toolbar (much higher up the tree). In this case as well I really don't want events bubbling back to the page content, but i definitely want the Form context to go with it. My implementation btw is some Slot-esque thing using context...

large gif 640x320

@sebmarkbage unstable_renderSubtreeIntoContainer allowed direct access to the top of the hierarchy regardless of a component's position, either within the hierarchy or as part of a separate packaged framework.

Comparatively, I see a few problems with the Slot solution:

  • The solution assumes you have access to the position of the hierarchy where it is "ok" to bubble events. This is definitely not the case for components and component frameworks.
  • Assumes it is "ok" to bubble events at any other level of the hierarchy.
  • Events will still bubble up from Slot's position. (as @craigkovatch mentioned)

I also have a usecase (probably similar to ones already mentionned).

I have a surface where users can select things with his mouse with a "lasso". This is basically 100% width/height and is at the root of my app and use onMouseDown event. In this surface there are also buttons that open portals like modals and dropdowns. A mouseDown event inside the portal is actually intercepted by the lasso selection component at the root of the app.

I see many to fix the problem:

  • render the portal one step above the root lasso component, but this is not very convenient and would probably need to resort to a context-based lib like react-gateway? (or maybe the slot system mentionned).
  • stop propagation manually inside the portal root, but could lead to unwanted side-effects mentionned above
  • ability to stop propagation in React portals (+1 btw)
  • filter out events when they come from a portal

For now my solution is to filter out the events.

const appRootNode = document.getElementById('root');

const isInPortal = element => isNodeInParent(element, appRootNode);


    handleMouseDown = e => {
      if (!isInPortal(e.target)) {
        return;
      }
      ...
    };

This will clearly not be the best solution for all of us and will not be very nice if you have nested portals, but for my current usecase (which is the only one currently) it works. I don't want to add a new context lib or do a complex refactor to solve this. Just wanted to share my solution.

I've been able to accomplish blocking event bubbling as noted elsewhere in this thread.

But another seemingly thornier issue I'm running into is the onMouseEnter SyntheticEvent, which does not bubble up. Rather it traverses from the common parent of the from component to the to component as described here. This means that if the mouse pointer enters from outside of the browser window, every onMouseEnter handler from the top of the DOM down to the component in createPortal will be triggered in that order, causing all sorts of events to fire that never did with unstable_renderSubtreeIntoContainer. Since onMouseEnter doesn't bubble up, it can't be blocked at the Portal level. (This did not seem to an issue with unstable_renderSubtreeIntoContainer as the onMouseEnter event did not respect virtual hierarchy and did not sequence through the body content, rather descended directly into the subtree.)

If anyone has any ideas on how to prevent onMouseEnter events from propagating from the top of the DOM hierarchy or divert directly into the portal subtree, please let me know.

@JasonGore I've also noticed this behavior.

For instance.

I have a context menu that is rendered when a div triggers onMouseOver, then I open a Modal with createPortal by clicking one of the items in the menu. When I bring the mouse out of the browser window, the onMouseLeave event propagates up to the context menu, closing the context menu (and thus the Modal)...

Had the same issue where I had a list item that I wanted the entirety to be clickable (as a link), but wanted a delete button on labels underneath the name which would open a modal to confirm.

screenshot 2018-10-31 at 11 42 47

My only solution was to prevent bubbling on the modal div like so:

// components/Modal.js

onClick(e) {
    e.stopPropagation();
}

return createPortal(
        <div onClick={this.onClick} ...
            ...

It'll prevent bubbling on every modal yes, but I have no case where I would want that to happen yet so it works for me.

Are there potential issues with this approach?

@jnsandrew don't forget there are ~50 other event types that bubble 🙃

Just hit this. It seems awkward to me that React would behave in its own way that is different to DOM event bubbling.

+1 to this. We are using React.createPortal to render inside the iframe, (for both styles and events isolation) and not be able to prevent events to bubble out of the box is a bummer.

Looks like this is the 12th most-thumbed up issue in React's backlog. At least the docs are open about it https://reactjs.org/docs/portals.html#event-bubbling-through-portals - altho they don't mention the downsides or workarounds and instead notes that it allows "more flexible abstractions":

The docs should at least explain that this can cause problems and suggest workarounds. In my case, it's a pretty straightforward use case, using https://github.com/reactjs/react-modal: I have buttons which open things like dropdowns, and inside these are buttons which create modals. Clicking the modal bubbles up to top button, causing it to do unwanted stuff. The modals are encapsulated into a cohesive component and pulling out the portaled part breaks that encapsulation, creating a leaky abstraction. One workaround might be flipping a flag to disable these buttons while the modal is open. And of course I can also stopPropagation as suggested above altho in some cases I might not want to do that.

I'm not sure how helpful bubbling and capture are in general (altho I know React relies on bubbling under the hood) - they certainly have a storied history, but I'd rather pass a callback or propagate a more specific event (e.g., redux action) than bubble up or capture down, since such things probably through a bunch of unnecessary intermediaries. There's articles like https://css-tricks.com/dangers-stopping-event-propagation/ and I work on apps which do depend on propagation to the body, mostly to close things when clicking "outside", but I'd rather put an invisible overlay over everything and close on clicking that. Of course, I couldn't use React's Portal to create such an invisible overlay...

There's also a maintenance nightmare here -- as new events get added to the DOM, any portals "sealed" with the above-discussed technique will "leak" those new events until maintainers can go add them to the (extensive) blacklist.

There's a major design issue here that needs to be addressed. An ability to opt-in or opt-out of cross-Portal bubbling still seems the best API option to me. Not sure about implementation difficulty therein, but we're still getting production bugs around this in Tableau, over a year later.

Spend 2 hours trying to find out why my form from modal was submitting another form.
Finally I figured it out thanks to that issue!

I really struggle to see when onSubmit propagation may be necessary. Most likely it will always be more like a bug rather then a feature.

At least it is worth to add some warning info to react docs. Something like this:
While Event Bubbling Through Portals is a great feature some times you may want to prevent some event propagation. You can achieve that by adding onSubmit={(e) => {e.stopPropagation()}}

+1 to this also. We are using draftjs heavilly with clickable text showing modals. And all events on modal like focus, select, change, keypress etc. just exploding draftjs with errors.

IMO, the event-proxying behavior is fundamentally broken (and is causing me bugs as well), but I recognize that this is controversial. This thread pretty strongly suggests that there's a need for a portal which wormholes context but not events. Does the core team agree? Either way, what's the next step here?

I cannot really realize, why propagating events from portal is intended behaviour? This is completely stands against the main idea of propagation. I thought that portals were created exactly to avoid this kind of things (like manual nesting, events propagation, etc.).

I can confirm, that if you put the portal near the elements tree, then it WILL propagate events:

class SomeComponent extends React.Component<any, any> {
  render() {
    return <>
      <div className="some-tree">
        // Portal here will bubble events
      </div>
      // Portal here will also bubble events, just checked
    </>
  }
}

+1 for this feature request

In DOM, events bubble up the DOM tree. In React, events bubble up the component tree.

I rely on the existing behaviour quite a bit, an example of which is popouts which may be nested; they're all portals to avoid issues with overflow: hidden, but in order to get the popout to behave correctly I need to detect external clicks to the popout component (which is different to detecting clicks outside of the rendered DOM elements). There may be better examples.

I think the robust discussion here has made it clear that there are good reasons to have both behaviors. Because createPortal renders a React component inside of a "plain DOM" container node, I don't think it would be workable for React's Synthetic Events to propagate out of a Portal up the plain-old-DOM tree.

Since Portals have been out for a long time now, it's probably too late to change the default behavior to "don't propagate past the portal boundary".

Based on all the discussion so far, my simplest proposal is then (still): add an optional flag to createPortal that prevents any event propagation past the portal boundary.

Something more robust might be the ability to provide a whitelist of events that should be allowed to "poke through" the boundary, while stopping the rest.

@gaearon Are we at the point that the React team could actually take this on? This is a top-10 issue but we haven't heard anything from yous on this in quite a while.

I want to add my support to this, and disagree with @sebmarkbage's comments from last year arguing that portaling both React context and DOM event bubbling makes more conceptual sense than portaling just context.

The ability to portal context from one place in the DOM to another is useful for implementing all manner of overlays such as tooltips, dropdowns, hover cards, and dialogs, where the content of the overlay is described by, and rendering in the context of, the trigger. Since context is a React concept, this mechanism solves a React problem. On the other hand, the ability to portal DOM event bubbling from one place in the DOM to another is a fancy trick that lets you pretend the DOM structure is other than what you explicitly set it up to be. This solves a problem with using DOM event bubbling for delegation, when you want to delegate to a different part of the DOM. Probably you should be using callbacks (or context) anyway, if you have React, rather than relying on DOM events bubbling from inside to outside of the overlay. As others have pointed out, rarely do you want to "reach in" and handle an event happening inside the overlay, intentionally or unintentionally.

DOM event bubbling primarily solves a problem of matching DOM events to DOM targets. Every click is actually a click on a whole set of nested elements. It's not best thought of as a high-level delegation mechanism, IMO, and using DOM events to delegate across React component boundaries is not great encapsulation, unless the components are small private helper components used to render predictable bits of DOM.

event.target === event.currentTarget helps me solve this problem. But this is realy headache.

This bit me today while trying to migrate a popover component using unstable_renderSubtreeIntoContainer to use createPortal. The component in question contains draggable elements and is rendered as a descendant of another draggable element. This means both the parent and popover elements contain mouse & touch event handlers, which both started firing when interacting with the portal'ed popover.

Since unstable_renderSubtreeIntoContainer is being deprecated(?) an alternative solution is necessary - none of the workarounds presented above appears to be viable long term solutions.

Hey! Thank for all this suggestions guys!
It helped me to fix one of my troubles.
Would you like to read great and informative article about the importance and abilities of React team? I guess it will be useful for everybody who is interested in development. Good luck!

IMO it's more often you want a portal to give you access to context, but not bubble up events. Back when we used Angular 1.x, we wrote our own popup service that would take $scope and a template string, and compile/render that template and append it to the body. We implemented all of our application's popups/modals/dropdowns with that service, and not once did we miss the lack of event bubbling.

The stopPropagation() workaround appears to prevent native event listeners on window from triggering (in our case added by react-dnd-html5-backend).

Here's a minimal repro of the issue: https://codepen.io/mogel/pen/xxKRPbQ

If there's no plan of providing a way to avoid the synthetic bubbling across portals, perhaps someone has a workaround which doesn't break native event bubbling?

The stopPropagation() workaround appears to prevent native event listeners on window from triggering

Correct. :(

If there's no plan of providing a way to avoid the synthetic bubbling across portals

Despite the core team's silence, I, and many others on this thread, _really hope_ that there are such plans.

perhaps someone has a workaround which doesn't break native event bubbling?

My team's workaround has been to ban portals entirely because of this glaring issue. We present panes with a hook into a container that lives within the app’s other contexts, so you get root level contexts for free; any others we pass across manually. Not great, but better than pointless whack-a-mole event handlers.

It's been 17 months since the last response by anyone from the core team. Maybe a ping could get this issue some attention :) @sebmarkbage or @gaearon

My team's workaround has been to ban portals entirely because of this glaring issue. We present panes with a hook into a container that lives within the app’s other contexts, so you get root level contexts for free; any others we pass across manually. Not great, but better than pointless whack-a-mole event handlers.

I can't think of any generic approaches to passing context down into the "fake portal" via props without going back to relying on cascading props :(

Countless were the bugs I caught on https://github.com/reakit/reakit that were related to this issue. I use React Portal a lot and I can't think of a single case where I wanted event bubbling from portals to their parent components.

My workaround has been either checking it inside my parent event handlers:

event.currentTarget.contains(event.target);

Or using native events instead:

const onClick = () => {};
React.useEffect(() => {
  ref.current.addEventListener("click", onClick);
  return () => ref.current.removeEventListener("click", onClick);
});

I use those approaches internally in the library. But none of them are ideal. And, since this is an open source component library, I can't control how people pass event handlers to the components.

An option to disable event bubbling would solve all those problems.

I hacked together a semi-workaround which blocks React bubbling while also retriggering a clone of the event on window. It appears to work in Chrome, Firefox and Safari on OSX, but IE11 is left out due to not allowing event.target to be manually set. So far it only cares about Mouse, Pointer, Keyboard and Wheel events. Not sure if drag events would be possible to clone.

Unfortunately it's not usable in our codebase since we require IE11 support, but maybe someone else could adapt it for their own use.

What makes this especially mind boggling, is that the 'default' behaviour bubbles _down_ the component tree again. Take the following tree:

<Link>
   <Menu (portal)>
      <form onSubmit={...}>
         <button type="submit">

I have been pulling my hair out for hours, as to why with this exact combination of components my form’s onSubmit is never called — regardless if I click the submit button or press Enter in an input field inside the form.

Finally, I discovered it is because the React Router Link component has an onClick implementation that does e.preventDefault() to prevent the browser from reloading. However, this has the unfortunate side effect of also blocking the default behaviour of the click on the submit button, which happens to be submitting the form. So what I learned today is the onSubmit is actually called by the browser, as a default action to pressing the submit button. Even when you press enter, it triggers a click on the submit button, thus triggering a form submit.

But you see how the event bubbling order makes this really really weird.

  1. <input> [keypress enter]
  2. <button type="submit"> [simulated click]
  3. <Menu> [event propagates outside portal]
  4. <Link> [propagation reaches parent Link]
  5. <Link> [calls e.preventDefault()]
  6. => default browser response to submit button click is canceled
  7. => form is not submitted

This happens even though we already passed the button and form in the DOM, and Link has nothing to do with it and also didn’t intend to block this behaviour at all.

The solution for me (if anyone runs into the same problem) was the commonly used solution of wrapping the <Menu> content in a div with onClick={e => e.stopPropagation()}. But my point is I lost a lot of time tracking down the issue because the behaviour is really unintuitive.

The solution for me (if anyone runs into the same problem) was the commonly used solution of wrapping the <Menu> content in a div with onClick={e => e.stopPropagation()}. But my point is I lost a lot of time tracking down the issue because the behaviour is really unintuitive.

Yup — each _individual instance of the problem_ has the same easy solution, _once you've experienced the bug and correctly identified it_. It's a very steep-walled pit of failure that the React team has carved out here, and it's frustrating to not hear any acknowledgement of that from them.

I've spent several days trying to debug another issue with mouseenter bubbling out of portals unexpectedly. Even with onMouseEnter={e => e.stopPropagation()} on the portal'ed div, the events are still bubbling out to the owning Button, as in https://github.com/facebook/react/issues/11387#issuecomment-340009465 (the first comment on this issue). mouseenter/mouseleave aren't supposed to bubble in the first place...

Perhaps even stranger, when I see a mouseenter synthetic event bubble out of a portal to a button, e.nativeEvent.type is mouseout. React is triggering a non-bubbling synthetic event based on a bubbling native event -- and despite the stopPropagation being called on the synthetic event.

@gaearon @trueadm this issue has caused consistent, enormous frustration for over two years now. This thread is one of the top active issues on React. Please, could someone from the team contribute here?

In my case opening the Window component by clicking on a Button made the window dissapear since the clicking on Window caused a click on button which caused the state change

I am new with React, I mostly use a jQuery and vanillia JS but this is mind blowing bug. There may be like 1% of cases when this behaviour would be expected...

I like the two solutions from @diegohaz, but I still think createPortal should have an option to stop event bubbling.

My particular use case was with a tooltip's onMouseLeave and onMouseEnter handlers being triggered by its child's portal descendant -- which wasn't desired. Native events fixed this by ignoring the portal descendants since they aren't dom descendants.

+1 for the option to stop bubbling in portals. It was suggested to just place the portal as a sibling (instead of a child) to the component where the event listener is originating, but i think that doesn't work in many use cases (including mine).

It finally looks like ReactDOM.unstable_renderSubtreeIntoContainer will be removed, meaning there soon won't be any reasonable workarounds left for this issue...

^ help us @trueadm -nobi you're our only hope

It looks like pinging them on GitHub doesn't work 😞
Perhaps somebody with an active Twitter account could tweet about this, tagging one of the contributors?

Adding my +1 for this issue. At Notion, we currently use a custom portal implementation that predates React.createPortal, and we manually forward our context providers to the new tree. I tried to adopt React.createPortal but was blocked by the unexpected bubbling behavior:

@sebmarkbage's suggestion to move the <Portal> outside of the <MenuItem> component to become a sibling only solves the issue for a single nesting level. The issue remains if you have multiple nested (eg) menu items that portal out sub-menus.

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!

Bump.

Dan left a comment on a related issue:

@mogelbrod I don't currently have anything to add to that, but something like this (#11387 (comment)) seems reasonable to me if you're migrating an existing component.

Followup by Dan in the same issue:

Thanks for the context about workaround. Since you already have that domain knowledge, the best next step is probably to write an RFC for the behavior you want and the alternatives you considered: https://github.com/reactjs/rfcs. Keep in mind that an RFC saying "let's just change this" is unlikely to be helpful. Writing a good RFC requires both understanding of why we have the current behavior, _and_ a plan to change it in a way that suits your use cases without regressing on others.

Regardless of that, unstable_renderSubtreeIntoContainer is not supported, so let's untangle these two discussions. We will not be adding propagation of Context to it because the whole API is frozen and will not be updated.

We should definitely publish a React RFC to suggest the addition of the discussed flag, or perhaps another solution. Does anybody feel particularly interested in drafting one (perhaps @justjake, @craigkovatch, or @jquense)? If not I'll see what I can come up with!

While I am interesting in evolving this API I don't have interest in drafting a RFC. Mostly bc it's a bunch of work and there is almost no chance it would be accepted, I don't think the core team actually considers RFCs that aren't already on their roadmap.

@jquense I don't think this is accurate. Yes, we're unlikely to merge an RFC that doesn't align with the vision since adding a new API is always cross-cutting and influences all the other planned features. And it's fair we don't often comment on the ones that don't work. However, we do read through them, and especially when we approach a topic that the ecosystem has more expertise in. As examples, https://github.com/reactjs/rfcs/pull/38, https://github.com/reactjs/rfcs/pull/150, https://github.com/reactjs/rfcs/pull/118, https://github.com/reactjs/rfcs/pull/109, https://github.com/reactjs/rfcs/pull/32 have all been influential in our thinking even though we haven't explicitly commented on them.

In other words, we approach RFCs in part as a community research mechanism. This comment from @mogelbrod (https://github.com/facebook/react/issues/16721#issuecomment-674748100) about why the workaround is annoying is exactly the kind of thing we'd like to see in an RFC. A consideration of existing solutions and their drawbacks can be more valuable than a concrete API suggestion proposal.

@gaearon My comment is not to suggest that the team does not listen to outside feedback. Ya'll do a good job of that. I do think my comment is accurate tho. The _process_ as it plays out on the RFC repo does not result in accepted RFC's from other folks. Looking at which RFCs are merged, it's entirely core team members or fb employees and no one else. Features that do make it, are usually a bit different and do not participate the RFC process at all (e.g. isomorphic ID's).

I'm very glad to hear ya'll do look at the other RFC's and that they contribute into design for features, but "We were influenced by these outside RFC's even though we never commented on them", I think illustrates my point, not challenge it.

In other words, we approach RFCs in part as a community research mechanism.

That's super reasonable, but not what the RFC repo says _its_ approach is, and not how other people generally think of RFC's. The RFC process are usually link and point of communication between the team and community, as well as something of an even playing field in terms of feature consideration and process.

Larger points about community governance aside. Asking folks, to spend time writing up detailed proposals and then defend them to other outside participants while being met with silence from the react team is disheartening and actively strengthens the impression that FB only cares about it's own needs for OSS. Which stinks because i know ya'll don't feel or design that way.

If the RFC process supposed to be: "here is where you can outline your concerns and use cases and we will read them, when/if we reach a point of being able to implement this feature". Honestly, that's a fine approach. I think the community would benefit from that that being explicitly spelt out, otherwise ppl will (and do) assume the same level of involvement and participation that other RFC processes often have and then be actively discouraged when that doesn't play out. I certainly have that impression even with slightly more insight than other contributors.

Sure, I think I agree with all of that. I don't want to turn this into a meta-thread but just saying that since people keep pinging about this thread, the most actionable thing to do to move this forward is to write a proposal for how it should work that takes concerns on both sides into account and compiles a deep understanding of the problem space. I totally understand if this is not something people would like to sink their teeth in (in part based on how we respond to RFCs), which is why I have not suggested it earlier — but since I keep getting personal pings I wanted to suggest that as an option for someone who's motivated.

fair enough, this isn't the right place to get meta on RFCs :)

@gaearon this is the 6th most-upvoted Issue currently open on React, and the 4th most commented. It has been open since React 16 was released, and is just 2 months away from being 3 years old now. Yet there has been very little engagement from the React core team. It feels very dismissive to say "it's up to the community to propose a solution" after that much time and pain has passed and occurred. Please recognize that, though it does have some very useful applications, this behavior being the default was a design mistake. It should not be up to the community to RFC to fix it.

I regret commenting on this issue and I retract my suggestion about the community RFC. You're right it's probably a bad idea. I have to add that this issue has become very emotionally charged, and as a human being I personally find it difficult to engage with it — even though I understand it is important and a lot of people feel strongly about it.

Let me briefly reply on the state of this thread.

First, I want to apologize to the people who commented and have been frustrated by us not continuing the follow-ups in this thread. If I were reading this issue from the outside, my impression would probably have been that the React team has made a mistake, is unwilling to admit it, and are willing to sit on a simple solution ("just add one boolean, how hard can it be!") for over two years because they don't care about the community. I can totally understand how you might come to this conclusion.

I know this issue is highly upvoted. This has been brought up multiple times in this thread, maybe from the perspective that if React team had known this is a big pain point, we would have addressed it sooner. We know this is a pain point — people regularly message us privately about it, or hold it up as an example of how the React team doesn't care about the community. While I fully acknowledge that the silence has been frustrating, the mounting pressure to "just do something" has made it more difficult to engage with this issue productively.

This issue has workarounds — which makes it unlike a security vulnerability or a crash that has to be dealt with urgently. We know that wokarounds work (but aren't ideal and can be annoying) because we use some of them ourselves, especially around code that was written before React 16. I think we can agree that while this issue has undobtedly been frustrating to a large number of people, it is still in a different class of problems than a crash or a security issue that must be responded to within a concrete timeframe.

Additionally, I disagree with the framing that there is a simple solution we can implement tomorrow. Even if we consider the initial behavior a mistake (which I'm not sure I agree with), it means that the bar for having the next behavior handle the full variety of use cases is even higher. If we fix some cases but break others, we haven't made any progress, and have created a ton of churn. Keep in mind that we won't hear about the cases where the current behavior works well in this issue. We'll only hear about it after we break it.

To give you an example, the current behavior is actually really helpful for the declaratively focus management use case that we have been researching for quite some time. It is useful to treat focus/blur as happening "inside" a modal with regards to the part tree, despite it being a Portal. If we were to ship the "simple" createPortal(tree, boolean) proposal suggested in this thread, this use case would not work because the portal itself cannot "know" about which behavior we want. Any exploration into a possible solution needs to consider dozens of use cases and some of them are not even fully understood yet. This is necessary to do at some point for sure, but it is also a huge time commitment to do it right, and so far we haven't been able to focus on it.

Events in particular are a thorny area, e.g. we've just made a bunch of changes that address years' worth of issues, and this has been a big focus this year. But we can only do so many things at a time.

Generally, we as a team try to focus on few problems deeply, rather than on many problems shallowly. Unfortunately, this does mean that some conceptual flaws and gaps may not get filled for years because we are in the middle of fixing other important gaps or don't have an alternative design worked out that would have solved the problem for good. I know this is frustrating to hear, and it's a part of why I stayed away from this thread. Some other similar threads have turned into deeper explanations of the problems and possible solutions, which are helpful, but this one has mostly turned into a flood of "+1" and suggestions for a "simple" fix, which is why it has been difficult to engage with it meaningfully.

I know this isn't the answer people wanted to hear, but I hope it is better than no answer at all.

Another thing that's worth calling out is that some of the pain points described in this thread might have been solved by other means. For example:

Specifically: calling stopPropagation on the synthetic focus event to prevent it from bubbling out of the portal causes stopPropagation to also be called on the native focus event in React's captured handler on #document, which meant it did not make it to another captured handler on

React no longer uses capture phase for emulating bubbling, and also doesn't listen to events on the document anymore. So without dismissing the frustration, it will definitely be necessary to reevaluate everything that has been posted so far in the light of the other changes.

The native events still bubble, and since our React code is hosted inside of a mostly-jQuery application, the global jQuery keyDown handler on still gets the event.

Similarly, React 17 will attach events to the roots and portal containers (and actually stop the native propagation at that point) so I would expect to be solved too.

Regarding the points about renderSubtreeIntoContainer being removed. Literally its only difference from ReactDOM.render is that it propagates Legacy Context. Since any release that wouldn't include renderSubtreeIntoContainer would also not include Legacy Context, ReactDOM.render would stay a 100% identical alternative. This doesn't, of course, solve the broader issue, but I think the concern with renderSubtree specifically is somewhat misplaced.

@gaearon

Regarding the points about renderSubtreeIntoContainer being removed. Literally its only difference from ReactDOM.render is that it propagates Legacy Context. Since any release that wouldn't include renderSubtreeIntoContainer would also not include Legacy Context, ReactDOM.render would stay a 100% identical alternative. This doesn't, of course, solve the broader issue, but I think the concern with renderSubtree specifically is somewhat misplaced.

Now that you mentioned it, I wonder if the code below would be a valid and safe implementation for a React Portal without event bubbling:

function Portal({ children }) {
  const containerRef = React.useRef();

  React.useEffect(() => {
    const container = document.createElement("div");
    containerRef.current = container;
    document.body.appendChild(container);
    return () => {
      ReactDOM.unmountComponentAtNode(container);
      document.body.removeChild(container);
    };
  }, []);

  React.useEffect(() => {
    ReactDOM.render(children, containerRef.current);
  }, [children]);

  return null;
}

CodeSandbox with some tests: https://codesandbox.io/s/react-portal-with-reactdom-render-m22dj?file=/src/App.js

There is still an issue with not passing Modern Context through but this isn't a new issue (renderSubtree is also affected by it). The workaround there is to surround your tree with a bunch of context providers. Overall it's not ideal to nest trees, so I wouldn't recommend to shift way to this pattern in anything other than legacy existing code scenarios.

Once again, thank you so much for the writeup @gaearon!

It sounds like aggregating the list of broken cases + workarounds (updated for React v17) would be the most productive thing for somebody outside the core team (correct me if I'm wrong!).

I'm swamped the coming weeks but aim to do ASAP. If anybody else is able to do this earlier, or chime in with snippets (like @diegohaz just did), that would be awesome!

Aggregating a list of cases would definitely be useful, although I'd say it needs to include not only broken cases but also the cases where the current behavior makes sense.

If there is a public space to add to i'd be happy to add use cases from both our apps and as a UI library author. In general i agree with Dan that while sometimes annoying it's easy to work around. For cases where you do want React's bubbling it's very hard to do cover the case without React's help.

Aggregating a list of cases would definitely be useful, although I'd say it needs to include not only broken cases but also the cases where the current behavior makes sense.

I'd be happy to do include these if anybody can point me to some open source code/extracted code that relies on it! Like you mentioned previously it's a bit of a challenge to find since only people having issues with the current behaviour are involved on this issue 😅

If there is a public space to add to i'd be happy to add use cases from both our apps and as a UI library author. In general i agree with Dan that while sometimes annoying it's easy to work around. For cases where you do want React's bubbling it's very hard to do cover the case without React's help.

Any specific space you have in mind, or would sharing one codesandbox (or jsfiddle, etc.) per case work as a starter? I can try compiling them all once we've gathered some cases.

I started a thread here: https://github.com/facebook/react/issues/19637. Let's keep it focused on practical examples, while this one stays for a general discussion.

Was this page helpful?
0 / 5 - 0 ratings