React: createPortal:支持停止在 React 树中传播事件的选项

创建于 2017-10-27  ·  103评论  ·  资料来源: facebook/react

您要请求功能还是报告错误
功能,但也是一个错误,导致新的 API 破坏了旧的unstable_rendersubtreeintocontainer

目前的行为是什么?
我们无法阻止从门户到其 React 树祖先的所有事件传播。 我们带有模态/弹出框的层机制完全被破坏了。 例如,我们有一个下拉按钮。 当我们点击它时,点击打开弹出窗口。 我们还想在单击同一按钮时关闭此弹出窗口。 使用 createPortal,在弹出窗口内单击会触发单击按钮,然后它就会关闭。 在这种简单的情况下,我们可以使用 stopPropagation。 但是我们有很多这样的情况,我们需要对所有这些情况使用 stopPropagation。 此外,我们无法阻止所有事件。

什么是预期行为?
createPortal 应该有一个选项来停止通过 React 树传播的合成事件,而无需手动停止每个事件。 你怎么认为?

DOM Feature Request

最有用的评论

即使这对我来说似乎也不必要地复杂。 为什么不简单地向 createPortal 添加一个可选的布尔标志来阻止冒泡行为?

所有103条评论

此外,mouseOver/Leave 的传播看起来完全出乎意料。
image

你能把传送门移到按钮外吗?

例如

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

然后它不会通过按钮冒泡。

这是我的第一个想法,但是!)想象一下,我们在这样的组件容器中有 mouseEnter 处理程序:

image

使用unstable_rendersubtreeintocontainer我不需要处理ButtonWithPopover组件中的事件——mouseEnter 只是在鼠标真正进入div和按钮 DOM 元素时起作用,并且在鼠标悬停在弹出框上时不会触发。 使用门户,当鼠标悬停在弹出框上时会触发事件——实际上此时不会超过div 。 所以,我需要停止传播。 如果我在ButtonWithPopover组件中执行此操作,我将在鼠标悬停在按钮上方时中断事件触发。 如果我在 popover 中执行此操作,并且我正在为此应用程序使用一些常见的 popover 组件,则我还可以破坏其他应用程序部分的逻辑。

我真的不明白通过 React 树冒泡的目的。 如果我需要来自门户组件的事件——我可以简单地通过 props 传递处理程序。 我们用unstable_rendersubtreeintocontainer来做,效果很好。

如果我从反应树深处的某个按钮打开一个模态窗口,我将在模态下收到意外触发的事件。 stopPropagation也将停止在 DOM 中传播,并且我不会收到我真正希望被解雇的事件(

@gaearon我建议这与其说是功能请求,不如说是一个错误。 我们有许多由通过门户冒泡的鼠标事件引起的新错误(我们以前使用的是unstable_rendersubtreeintocontainer )。 即使使用额外的 div 层来过滤鼠标事件,其中一些也无法修复,因为例如我们依靠传播到文档的 mousemove 事件来实现可拖动的对话框。

在未来版本中解决这个问题之前,有没有办法解决这个问题?

我认为它被称为功能请求,因为门户网站当前的泡沫行为既是预期的,也是预期的。 目标是子树表现得像他们父母的真正孩子。

有帮助的是您认为当前实现无法提供或难以解决的其他用例或情况(例如您看到的情况)

我知道这种行为是有意为之,但我认为这是一个不可禁用的重大错误。

在我看来,使用 DOM 的库应该保留 DOM 实现行为而不是破坏它。

例如:

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>
  );
}

当我使用 DOM 时,我希望收到像 DOM 实现那样的事件。 在我的示例中,事件通过Portal传播,绕过它的 DOM 父级,这可以被视为一个bug

伙计们感谢您的讨论,但是我认为争论某些东西是否是错误并没有什么帮助。 相反,我会更有效率地讨论当前行为未满足的用例和示例,因此我们可以更好地了解当前的方式是否是未来的最佳方式。

总的来说,我们希望 API 能够处理各种不同的用例,同时希望不要过度限制其他用例。 我不能代表核心团队发言,但我认为让它可配置不是一个可能的解决方案。 通常 React 倾向于使用一致的 API,而不是可配置的 API。

我也明白这种行为不是 DOM 的工作方式,但我认为这本身并不是说它不应该这样的一个很好的理由。 很多 react-dom 的行为与 DOM 的工作方式不同,可能事件已经与原生版本不同。 onChange完全不同于原生的 change 事件,并且所有的 react 事件都会冒泡,而不管类型如何,这与 DOM 不同。

相反,我会更有效率地讨论当前行为未满足的用例和示例

这是我们在迁移到 React 16 时被破坏的两个示例。

首先,我们有一个由按钮启动的可拖动对话框。 我试图在我们的 Portal 使用中添加一个“过滤”元素,它在任何鼠标 * 和键 * 事件上调用 StopPropagation。 但是,我们依赖于能够将 mousemove 事件绑定到文档以实现拖动功能——这是很常见的,因为如果用户以任何显着的速度移动鼠标,光标就会离开对话框的边界,您需要能够在更高级别捕获鼠标移动。 过滤这些事件会破坏此功能。 但是对于 Portals,鼠标和按键事件会从对话框内部冒泡到启动它的按钮,导致它显示不同的视觉效果,甚至关闭对话框。 我认为期望将通过 Portal 启动的每个组件绑定 10-20 个事件处理程序以停止此事件传播是不现实的。

其次,我们有一个弹出式上下文菜单,可以通过主鼠标单击或辅助鼠标单击来启动。 我们库的一个内部使用者将鼠标处理程序附加到启动此菜单的元素上,当然菜单也有用于处理项目选择的单击处理程序。 现在每次单击都会重新出现菜单,因为 mousedown/mousedown 事件会冒泡返回到启动菜单的按钮。

我不能代表核心团队发言,但我认为让它可配置不是一个可能的解决方案。 通常 React 倾向于使用一致的 API,而不是可配置的 API。

我恳请您(和团队)在这种特殊情况下重新考虑这一立场。 我认为事件冒泡对于某些用例会很有趣(尽管我想不出任何副手)。 但我认为它会在其他方面造成严重影响,并且会在 API 中引入严重的不一致。 虽然unstable_rendersubtreeintocontainer从来没有被超级支持,但它是每个人都用来在直接树之外渲染的,而且它不是这样工作的。 它被正式弃用而支持 Portals,但 Portals 以这种关键方式破坏了该功能,并且似乎没有简单的解决方法。 我认为这可以被公平地描述为非常不一致。

我也明白这种行为不是 DOM 的工作方式,但我认为这本身并不是说它不应该这样的一个很好的理由。

我明白你从哪里来,但我认为在这种情况下 (a) 这是一个基本行为,(b) 目前没有解决方法,所以我认为“DOM 不能以这种方式工作”是一个强有力的论点,如果不是一个完全令人信服的。

并且要明确:我要求将其视为错误主要是为了尽快将其优先修复。

我对 Portal 的心智模型是,它的行为就像它在树中的同一位置,但避免了诸如“溢出:隐藏”之类的问题,并避免了出于绘图/布局目的而滚动。

有许多类似的“弹出”解决方案在没有 Portal 的情况下内联发生。 例如,在它旁边展开一个框的按钮。

以 GitHub 上的“选择你的反应”对话框为例。 这是作为按钮旁边的 div 实现的。 现在工作正常。 然而,如果它想要一个不同的 z-index,或者被抬出包含这些注释的overflow: scroll区域,那么它需要改变 DOM 位置。 除非还保留了事件冒泡等其他事情,否则这种更改是不安全的。

“弹出窗口”或“弹出窗口”两种风格都是合法的。 那么当组件在布局中内联而不是浮动在布局之外时,您将如何解决相同的问题?

对我有用的解决方法是在门户渲染下直接调用stopPropagation

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

这对我很有用,因为我有使用门户的单个抽象组件,否则您将需要修复所有createPortal调用。

@methyl这假设您知道需要阻止冒泡树的每个事件。 在我提到的可拖动对话框的情况下,我们需要mousemove来冒泡到文档,而不是冒泡渲染树。

“弹出窗口”或“弹出窗口”两种风格都是合法的。 那么当组件在布局中内联而不是浮动在布局之外时,您将如何解决相同的问题?

@sebmarkbage我不确定这个问题是否有意义。 如果我有内联组件的这个问题,我不会内联它。

我认为这里的一些问题是renderSubtreeIntoContainer的一些用例被移植到createPortal当这两种方法在概念上做不同的事情时。 我认为门户的概念正在超载。

我同意在模态对话框的情况下,您几乎从不希望模态像打开它的按钮的子项一样。 触发器组件仅渲染它,因为它控制open状态。 我认为说门户实现因此是错误的,而不是说按钮中的createPortal不是正确的工具是错误的。 在这种情况下,Modal 不是触发器的子代,不应该像它那样呈现。 一个可能的解决方案是继续使用renderSubtreeIntoContainer ,另一个用户空间选项是在处理渲染模式的应用程序根附近有一个ModalProvider ,并传递(通过上下文)一个方法来渲染一个任意模态元素需要根

renderSubtreeIntoContainer不能从render或 React 16 中的生命周期方法调用,这几乎排除了它在我讨论的情况下的使用(事实上,我们所有的组件都是这样做在迁移到 16 时完全中断了)。 Portals 是官方推荐的: https: //reactjs.org/blog/2017/09/26/react-v16.0.html#break -changes

我同意 Portal 的概念可能已经超载了。 不过,我不确定我是否喜欢全局组件和上下文的解决方案。 这似乎可以通过 createPortal 上的标志轻松解决,指定事件是否应该通过。 这将是一个选择加入标志,可以保持 API 与 16+ 的兼容性。

我将尝试阐明我们的门户用例,以及为什么我们希望看到一个停止事件传播的选项。 在 ManyChat 应用程序中,我们使用门户来创建“层”。 我们为整个应用程序提供了由多种类型的组件使用的层系统:弹出窗口、下拉菜单、菜单、模态。 每个图层都可以暴露一个新图层,例如第二级菜单上的按钮可以触发模式窗口,其他按钮可以在其中打开弹出窗口。 在大多数情况下,层是解决其自身任务的 UX 的新分支。 当新图层打开时,用户应该与这个新图层交互,而不是与底部的其他人交互。 因此,对于这个系统,我们创建了一个用于渲染到图层的通用组件:

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)
  }
  ...
}

这个组件停止了 React 文档中所有事件类型的传播,它允许我们更新到 React 16。

这是否需要绑定到门户? 而不是沙盒门户,如果只有一个(例如) <React.Sandbox>...</React.Sandbox>呢?

即使这对我来说似乎也不必要地复杂。 为什么不简单地向 createPortal 添加一个可选的布尔标志来阻止冒泡行为?

@gaearon对于我们中的某些人

我要补充一点,我目前的想法是应该支持这两个用例。 在某些用例中,您需要上下文从当前父级流向子树,但不让该子树充当 DOM 方面的逻辑子级。 复杂的模态是最好的例子,你几乎从不希望来自模态窗口中的表单的事件传播到触发按钮,但几乎肯定需要传递的上下文(i18n、主题等)

我会说,这个用例 _could_ 主要通过一个更接近应用程序根的 ModalProvider 来解决,它通过createPortal呈现足够高的事件传播不会影响任何事情,但这开始感觉像是一种解决方法,而不是精心设计的架构。 它还使库提供的模态对用户来说更烦人,因为它们不再是自包含的。

我会在 API 方面添加 tho 我不认为createPortal应该同时做这两个,模态案例真的只想使用ReactDOM.render (旧 skool),因为它非常接近一棵独特的树 _except_通常需要上下文传播

由于使用了@kib357发布的解决方法,我们只需要修复外部应用程序焦点管理代码中的一个极其难以诊断的错误。

具体来说:在合成焦点事件上调用 stopPropagation 以防止它从门户中冒泡会导致 stopPropagation 在 React 捕获的#document 处理程序中的本地焦点事件上也被调用,这意味着它没有将其发送到<body>上的另一个捕获处理程序

Portals 中的新冒泡行为对我来说真的是少数情况。 无论是这种观点还是事实,我们能否在这个问题上获得一些牵引力? 也许@gaearon? 它已经四个月大了,引起了真正的痛苦。 我认为这可以被描述为一个错误,因为它是 React 16 中的一个破坏性 API 更改,没有完全安全的解决方法。

@craigkovatch我仍然很好奇你将如何解决我的内联示例。 假设弹出窗口将框的大小向下推。 内联某些东西很重要,因为它在给定其大小的情况下将布局中的某些东西向下推。 它不能只是悬停。

您可以潜在地测量弹出框,插入一个相同大小的空白占位符并尝试将其对齐在顶部,但这不是人们所做的。

因此,如果您的弹出框需要就地展开内容,例如在按钮旁边,您将如何解决? 我怀疑在那里工作的模式在两种情况下都适用,我们应该只推荐一种模式。

我认为总的来说这是在两种情况下都适用的模式:

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}
    </>;
  }
}

如果 Flyout 是一个门户,那么它可以工作并且当鼠标悬停在门户上时它不会获得任何鼠标悬停事件。 但更重要的是,如果它不是门户,它也可以工作,并且它需要是内联浮出控件。 不需要 stopPropagation。

那么这种模式对您的用例不起作用的原因是什么?

@sebmarkbage我们以完全不同的方式使用门户,渲染到作为<body>的最后一个子项安装的容器中,然后定位,有时使用 z-index。 React 文档表明这更接近设计意图; 即渲染到 DOM 中完全不同的地方。 在我看来,我们的用例不够相似,不足以讨论属于该线程的讨论。 但是,如果您想一起进行头脑风暴/故障排除,我很乐意在另一个论坛中进一步讨论。

不,我的用例是两者。 有时一个,有时另一个。 这就是为什么它是相关的。

<Flyout />可以选择是否渲染到 body 的最后一个子组件,但只要您将门户本身提升到悬停组件的兄弟组件而不是它的子组件,您的场景就可以工作。

我认为有一种看似合理的场景,这很不方便,您想要一种从深层嵌套组件传送事物的方法,但在这种情况下,您可能对上下文是来自中间点的上下文感到满意。 但我认为这是两个不同的问题。

也许我们需要一个插槽 API。

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" />
    </>;
  }
}

然后门户将获得 Bar 的上下文,而不是 DeepComponent。 上下文和事件冒泡仍然共享相同的树路径。

@sebmarkbage模态案例通常需要从它呈现的点开始的上下文。 我认为这是一个稍微独特的案例,该组件是呈现它的事物的逻辑子项,但_不是_结构性的(因为缺少更好的词),例如,您通常需要表单上下文(中继,formik,redux 表单)之类的东西, 无论如何) 但不能传递 DOM 事件。 人们最终也会在树中相当深的地方渲染这样的模态,在它们的触发器旁边,所以它们保持组件性和可重用性,而不是因为它们在结构上属于那里

我认为这个案例通常不同于 createPortal 服务的弹出/下拉案例。 Tbc 我认为门户的冒泡行为很好,但不适用于模态。 我也认为这可以用 Context 和某种 ModalProvider 处理得相当好,但这有点烦人,尤其是对于图书馆。

只要您将门户本身提升到悬停组件的兄弟组件而不是它的子组件,您的场景就可以工作。

不确定我是否遵循。 仍然存在例如 keyDown 事件通过意外的 DOM 树冒泡的问题。

@jquense请注意,在我的示例中,插槽仍位于 Bar 组件内,因此它将以<Form><Bar /></Form>类的形式从表单中获取其上下文。

即使门户呈现到文档正文中。

所以它就像两个间接(门户):deep -> Bar 的兄弟 -> 文档正文。

所以门户的上下文仍然是表单的上下文,事件冒泡链也是如此,但两者都不是在悬停的事物的上下文中。

是的很抱歉错过了😳如果我没看错,你仍然会在<Slot>上冒泡吗? 这肯定更好,尽管我确实认为在 Modal 对话框的情况下,人们可能不希望 _any_ 冒泡。 就像从屏幕阅读器的角度思考一样,您希望模态之外的所有内容在它启动时都被反转。 我不知道,我认为在这种情况下,冒泡是一个问题,没有人会期望在对话框内单击一下就可以在任何地方冒泡。

也许这里的问题不是门户,而是没有一种跨树共享上下文的好方法? 上下文中的一部分ReactDOM.render对于模态来说真的很好,而且无论如何可能是更“正确”的思考方式......

我的想法是有一些冒泡,因为它仍然从模态到 div 到正文到文档到窗口。 从概念上讲,超出框架到包含窗口等等。

在 ART 或 GL 渲染内容(以及在某种程度上 React Native)中,这不是理论上的,因为可能没有现有的支持树来获取这些语义。 所以需要有一种方式来说明是它冒泡的地方。

在某些应用程序中,模态中有模态。 例如,在 FB 中,聊天窗口可能位于模态上方,或者模态可能是聊天窗口的一部分。 所以即使是模态也有一些关于它在树中的位置的上下文。 它从来都不是完全独立的。

这并不是说我们不能对事件冒泡和上下文有两种不同的语义。 这一点都很明确,你可以在没有另一个的情况下传送一个等等。

保证它们都遵循相同的路径确实很强大,因为这意味着可以像在浏览器中一样为用户空间事件完全实现事件冒泡。

例如,今天的各种 Redux 上下文都会发生这种情况。 想象一下this.context.dispatch("Hover")是一个用户空间事件冒泡。 我们甚至可以将 React 事件作为上下文的一部分来实现。 认为我可以以同样的方式使用它是合理的,现在在各个方面,你都可以。 我认为如果我们真的 fork 这两个上下文,我们可能最终会得到另一个用户空间上下文 API,它遵循 DOM 结构并与普通上下文并行——如果它们真的如此不同的话。

所以这就是为什么我有点反对它以查看插槽是否足够,因为 a) 您需要明确说明无论如何都会发生上下文冒泡。 b) 它可以避免分叉世界和拥有两个完整的上下文系统。

具体来说:在合成焦点事件上调用 stopPropagation 以防止它从门户中冒泡会导致 stopPropagation 在 React 捕获的#document 处理程序中的本地焦点事件上也被调用,这意味着它没有将其发送到另一个捕获的处理程序

. 我们通过将处理程序向上移动到 #document 来修复,但我们过去特别避免这样做,以免踩到 React 的脚趾。

@craigkovatch ,您是否在文档上使用了onFocusCapture事件? 在我的解决方法中,不应停止捕获的事件。 您能否提供更详细的示例,说明情况如何以及您为解决问题所做的工作?
另外,我认为我的代码在停止blur事件方面存在问题——它不应该被停止。 因此,我将更深入地研究这个问题,并尝试找到更可靠的解决方案。

@kib357我并不是在暗示您的解决方法存在问题,我认为 React 中存在一个单独的错误(即,在冒泡阶段对合成焦点事件调用 stopPropagation 时,不应取消捕获阶段中原生焦点事件的传播)。

有问题的代码使用本机捕获事件侦听器,即document.body.addEventListener('focus', handler, true)

@craigkovatch听起来很有趣,因为您使用了捕获的处理程序。 但是,我没有任何想法为什么会发生这种情况。

所以,伙计们,我们有两种不同的使用门户渲染的场景:

  1. 防止在简单的小部件(如下拉按钮或一级菜单)中出现溢出:隐藏等 CSS 问题
  2. 为更强大的案例创建一个新的 UX 层,例如:
  3. 模态
  4. 嵌套菜单
  5. popovers-with-forms-with-dropdowns-... – 所有情况,当图层组合时

我认为当前的createPortal API 只满足第一种情况。 建议使用新的 React.render 作为第二个是不可用的——创建一个单独的应用程序非常糟糕,每个层的所有它的提供者。
我们可以提供哪些其他信息来帮助解决此问题?
createPortal API 中建议参数的缺点是什么?

@sebmarkbage我对插槽 API 的直接问题是:我能否同时将多个SlotContents插入一个Slot中? 在我们的界面中同时打开多个“弹出窗口”或“模态”并不少见。 在我的完美世界中, Popup API 看起来像这样:

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>
  </>
}

我们遇到了一个新问题,我完全找不到解决方法。 使用上面建议的“事件陷阱”方法,只有 React Synthetic 事件被阻止从门户冒泡。 本机事件仍然冒泡,并且由于我们的 React 代码托管在一个主要是 jQuery 的应用程序中,因此<body>上的全局 jQuery keyDown 处理程序仍然获取事件。

我试图通过像这样的引用将 event.stopPropagation 侦听器添加到 Portal 内的本机容器元素,但这完全中和了 Portal 内的所有 Synthetic 事件——我错误地认为 React 的顶级侦听器正在监视捕获阶段。

除了对 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>;
}

这取决于 ReactDOM 和 JQuery 的初始化顺序。 如果 JQuery 首先初始化,JQuery 的顶级事件处理程序将首先安装,因此它们将在 ReactDOM 的综合处理程序开始运行之前运行。

ReactDOM 和 JQuery 都更喜欢只有一个顶级侦听器,然后在内部模拟冒泡,除非有一些浏览器不会冒泡的事件,例如scroll

@Kovensky我的理解是 jQuery 没有像 React 那样进行“合成冒泡”,因此没有单个顶级侦听器。 我的 DOM 检查器也没有显示。 如果我弄错了,很想看看你在引用什么。

委托事件就是这种情况。 例如, $(document.body).on('click', '.my-selector', e => e.stopPropagation())

看,这可以在 React 中解决,如果有人让我相信这无法通过我上面提出的设计来解决,这需要对您的代码进行一些重构。 但除了试图找到快速修复的解决方法之外,我还没有看到任何无法完成的原因。

@sebmarkbage您的提议仅解决传播给直接所有者的事件的情况。 那棵树的其余部分呢?

这是一个我认为不能用 Slots 或 createPortal 很好解决的用例

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

这是一个具有类似但略有不同设置的 gif,其中我使用 createPortal 作为响应式站点,将表单字段移动到应用程序工具栏(在树的更高位置)。 在这种情况下,我真的不希望事件冒泡回页面内容,但我绝对希望 Form 上下文与之配合。 顺便说一句,我的实现是使用上下文的一些插槽式的东西......

large gif 640x320

@sebmarkbage unstable_renderSubtreeIntoContainer允许直接访问层次结构的顶部,无论组件的位置如何,无论是在层次结构内还是作为单独打包框架的一部分。

相比之下,我看到 Slot 解决方案存在一些问题:

  • 该解决方案假设您可以访问可以“确定”冒泡事件的层次结构位置。 组件和组件框架绝对不是这种情况。
  • 假设在层次结构的任何其他级别冒泡事件是“可以的”。
  • 事件仍会从 Slot 的位置冒泡。 (正如@craigkovatch提到的)

我还有一个用例(可能类似于已经提到的用例)。

我有一个表面,用户可以在其中使用带有“套索”的鼠标选择事物。 这基本上是 100% 宽度/高度,位于我的应用程序的根目录并使用onMouseDown事件。 在这个表面上还有一些按钮可以打开像模态框和下拉菜单这样的门户。 门户内部的 mouseDown 事件实际上是被应用程序根部的套索选择组件拦截的。

我看到很多解决问题:

  • 将门户呈现在根套索组件上方一步,但这不是很方便,可能需要求助于基于上下文的库,如 react-gateway? (或者可能是提到的插槽系统)。
  • 在门户根内手动停止传播,但可能会导致上面提到的不必要的副作用
  • 在 React 门户中停止传播的能力 (+1 btw)
  • 过滤掉来自门户的事件

现在我的解决方案是过滤掉事件。

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

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


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

这显然不是我们所有人的最佳解决方案,如果您有嵌套门户,也不会很好,但对于我当前的用例(目前唯一的用例)它有效。 我不想添加新的上下文库或进行复杂的重构来解决这个问题。 只是想分享我的解决方案。

我已经能够完成本线程中其他地方所述的阻塞事件冒泡。

但我遇到的另一个看似棘手的问题是onMouseEnter SyntheticEvent,它不会冒泡。 相反,它从所述的公共父横穿from组件到to如所描述的部件在这里。 这意味着如果鼠标指针从浏览器窗口外部进入,从 DOM 顶部到 createPortal 中的组件的每个onMouseEnter处理程序都将按该顺序触发,导致各种事件触发从来没有用过unstable_renderSubtreeIntoContainer 。 由于onMouseEnter不会冒泡,因此无法在 Portal 级别进行阻止。 (这似乎不是unstable_renderSubtreeIntoContainer的问题,因为onMouseEnter事件不尊重虚拟层次结构,也没有对正文内容进行排序,而是直接下降到子树中。)

如果有人对如何防止onMouseEnter事件从 DOM 层次结构的顶部传播或直接转移到门户子树有任何想法,请告诉我。

@JasonGore我也注意到了这种行为。

例如。

我有一个上下文菜单,当 div 触发 onMouseOver 时会呈现该菜单,然后我通过单击菜单中的一项来打开带有 createPortal 的 Modal。 当我将鼠标移出浏览器窗口时, onMouseLeave 事件会传播到上下文菜单,关闭上下文菜单(以及 Modal)...

遇到了同样的问题,我有一个列表项,我希望整个列表项都可以点击(作为链接),但希望在名称下方的标签上有一个删除按钮,这将打开一个模式进行确认。

screenshot 2018-10-31 at 11 42 47

我唯一的解决方案是防止像这样在模态 div 上冒泡:

// components/Modal.js

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

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

它会防止在每个模态上冒泡,是的,但我不希望这种情况发生,所以它对我有用。

这种方法是否存在潜在问题?

@jnsandrew别忘了还有大约 50 种其他事件类型会冒泡🙃

就打这个。 在我看来,React 会以不同于 DOM 事件冒泡的方式运行,这似乎很尴尬。

+1对此。 我们使用React.createPortal在 iframe 内部进行渲染,(对于样式和事件隔离)并且无法防止事件冒泡开箱即用是一个无赖。

看起来这是 React 待办事项中第 12 大点赞的问题。 至少文档是开放的https://reactjs.org/docs/portals.html#event -bubbling-through-portals - 尽管他们没有提到缺点或解决方法,而是指出它允许“更灵活的抽象”:

文档至少应该解释这可能会导致问题并建议解决方法。 就我而言,这是一个非常简单的用例,使用https://github.com/reactjs/react-modal :我有按钮可以打开下拉菜单之类的东西,在这些按钮中可以创建模态。 单击模态气泡到顶部按钮,使其执行不需要的操作。 模态被封装到一个有凝聚力的组件中,拉出入口部分会破坏封装,创建一个有漏洞的抽象。 一种解决方法可能是在模式打开时翻转标志以禁用这些按钮。 当然,我也可以按照上面的建议停止传播,但在某些情况下我可能不想这样做。

我不确定冒泡和捕获一般有多大帮助(尽管我知道 React 依赖于引擎盖下的冒泡)-它们肯定有一段传奇的历史,但我宁愿传递回调或传播更具体的事件(例如, redux action)而不是冒泡或捕获,因为这些事情可能通过一堆不必要的中介。 有像https://css-tricks.com/dangers-stopping-event-propagation/这样的文章,我开发的应用程序依赖于身体的传播,主要是在点击“外面”时关闭东西,但我宁愿在所有内容上放置一个不可见的叠加层并在单击时关闭。 当然,我不能用 React 的 Portal 来创建这样一个不可见的叠加层……

这里还有一个维护噩梦——随着新事件被添加到 DOM,任何使用上述技术“密封”的门户都会“泄漏”这些新事件,直到维护者可以将它们添加到(广泛的)黑名单中。

这里有一个主要的设计问题需要解决。 选择加入或选择退出跨门户冒泡的能力对我来说仍然是最好的 API 选择。 不确定其中的实施难度,但一年多后,我们仍然在 Tableau 中遇到与此相关的生产错误。

花 2 个小时试图找出为什么我的模态表单提交了另一个表单。
最后我想通了多亏了这个问题!

我真的很难看出何时可能需要onSubmit传播。 很可能它总是更像是一个错误而不是一个功能。

至少值得添加一些警告信息来响应文档。 像这样的东西:
虽然通过 Portal 进行事件冒泡是一项很棒的功能,但有时您可能希望阻止某些事件传播。 您可以通过添加onSubmit={(e) => {e.stopPropagation()}}

对此也有 +1。 我们使用draftjs heavilly带有可点击文字显示模态。 模式上的所有事件,如焦点、选择、更改、按键等,都只会导致 Draftjs 出现错误。

IMO,事件代理行为从根本上被破坏(并且也给我带来了错误),但我承认这是有争议的。 这个帖子强烈暗示需要一个传送门来处理上下文而不是事件。 核心团队同意吗? 无论哪种方式,下一步是什么?

我无法真正意识到,为什么从门户传播事件是预期行为? 这完全违背了传播的主要思想。 我认为创建门户正是为了避免这种事情(例如手动嵌套、事件传播等)。

我可以确认,如果您将门户放在元素树附近,那么它将传播事件:

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 此功能请求

在 DOM 中,事件会在 DOM 树中向上冒泡。 在 React 中,事件会在组件树中冒泡。

我相当依赖现有的行为,其中一个例子是可能嵌套的弹出窗口; 它们都是避免overflow: hidden问题的门户,但为了让弹出窗口正确运行,我需要检测弹出组件的外部点击(这与检测渲染DOM 元素之外的点击不同) . 可能还有更好的例子。

我认为这里的激烈讨论清楚地表明,有充分的理由同时拥有这两种行为。 因为createPortal在“普通 DOM”容器节点内呈现 React 组件,我认为 React 的合成事件从 Portal 传播到普通 DOM 树上是不可行的。

由于 Portal 已经推出很长时间了,现在将默认行为更改为“不传播超过 Portal 边界”可能为时已晚。

基于到目前为止的所有讨论,我最简单的建议是(仍然):向 createPortal 添加一个可选标志,以防止任何事件传播超过门户边界。

更强大的功能可能是提供事件白名单的能力,这些事件应该被允许“穿透”边界,同时停止其余事件。

@gaearon我们是否到了 React 团队可以真正承担这个问题的地步? 这是排名前 10 的问题,但我们已经有很长一段时间没有收到您的任何消息了。

我想对此表示支持,并且不同意@sebmarkbage去年的评论,认为门户化 React 上下文和 DOM 事件冒泡比门户化上下文更具概念意义。

从 DOM 中的一个位置到另一个位置的门户上下文的能力对于实现各种叠加方式非常有用,例如工具提示、下拉菜单、悬停卡和对话框,其中叠加的内容由以下内容描述并在其上下文中呈现,触发。 由于上下文是一个 React 概念,这个机制解决了一个 React 问题。 另一方面,将 DOM 事件从 DOM 中的一个地方冒泡到另一个地方的能力是一种奇特的技巧,它可以让您假装 DOM 结构与您明确设置的不同。 这解决了当您想要委托给 DOM 的不同部分时使用 DOM 事件冒泡进行委托的问题。 如果您有 React,可能无论如何您都应该使用回调(或上下文),而不是依赖于从覆盖层内部到外部冒泡的 DOM 事件。 正如其他人指出的那样,您很少想有意或无意地“进入”并处理覆盖层内发生的事件。

DOM 事件冒泡主要解决 DOM 事件与 DOM 目标匹配的问题。 每次点击实际上是对一整套嵌套元素的点击。 最好不要将其视为高级委托机制,IMO,并且使用 DOM 事件跨 React 组件边界进行委托并不是很好的封装,除非组件是用于呈现可预测的 DOM 位的小型私有辅助组件。

event.target === event.currentTarget 帮我解决了这个问题。 但这真的很头疼。

今天在尝试使用unstable_renderSubtreeIntoContainer迁移 popover 组件以使用createPortal这有点让我失望。 有问题的组件包含可拖动元素并呈现为另一个可拖动元素的后代。 这意味着父元素和 popover 元素都包含鼠标和触摸事件处理程序,它们都在与门户的 popover 交互时开始触发。

由于unstable_renderSubtreeIntoContainer已被弃用(?) 一个替代解决方案是必要的 - 上面提出的解决方法似乎都不是可行的长期解决方案。

嘿! 感谢所有这些建议家伙!
它帮助我解决了我的一个麻烦。
您想阅读有关React 团队的重要性和能力的精彩而翔实的文章吗? 我想这对每个对开发感兴趣的人都会有用。 祝你好运!

IMO 更常见的是,您希望门户可以让您访问上下文,而不是冒泡事件。 回到我们使用 Angular 1.x 时,我们编写了自己的弹出服务,该服务将接受$scope和一个模板字符串,然后编译/渲染该模板并将其附加到正文中。 我们使用该服务实现了所有应用程序的弹出窗口/模式/下拉菜单,而且我们一次也没有错过缺少事件冒泡的情况。

stopPropagation()解决方法似乎阻止触发window上的本机事件侦听器(在我们的示例中由react-dnd-html5-backend )。

这是该问题的最小重现: https :

如果没有计划提供一种方法来避免跨门户的合成冒泡,也许有人有一种不会破坏原生事件冒泡的解决方法?

stopPropagation() 解决方法似乎是为了防止触发窗口上的本机事件侦听器

正确的。 :(

如果没有计划提供一种方法来避免跨门户的合成冒泡

尽管核心团队保持沉默,但我和此线程上的许多其他人_真的希望_有这样的计划。

也许有人有一种不会破坏本机事件冒泡的解决方法?

由于这个明显的问题,我的团队的解决方法是完全禁止门户。 我们将带有挂钩的窗格呈现在应用程序的其他上下文中的容器中,因此您可以免费获得根级别的上下文; 我们手动通过的任何其他人。 不是很好,但比毫无意义的 whack-a-mole 事件处理程序更好。

距离核心团队的任何人最后一次回应已经过去了 17 个月。 也许 ping 可以让这个问题得到一些关注:) @sebmarkbage或 @gaearon

由于这个明显的问题,我的团队的解决方法是完全禁止门户。 我们将带有挂钩的窗格呈现在应用程序的其他上下文中的容器中,因此您可以免费获得根级别的上下文; 我们手动通过的任何其他人。 不是很好,但比毫无意义的 whack-a-mole 事件处理程序更好。

我想不出任何通用的方法来通过 props 将上下文传递到“假门户”而不回到依赖级联 props :(

我在https://github.com/reakit/reakit上发现了无数与此问题相关的错误。 我经常使用 React Portal,但我想不出一个案例,我希望事件从门户冒泡到其父组件。

我的解决方法是在我的父事件处理程序中检查它:

event.currentTarget.contains(event.target);

或者改用原生事件:

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

我在图书馆内部使用这些方法。 但它们都不是理想的。 而且,由于这是一个开源组件库,我无法控制人们如何将事件处理程序传递给组件。

禁用事件冒泡的选项将解决所有这些问题。

我编写了一个半解决方法,它阻止了 React 冒泡,同时还在window上重新触发了事件的克隆。 它似乎适用于 OSX 上的 Chrome、Firefox 和 Safari,但由于不允许手动设置event.target ,IE11 被排除在外。 到目前为止,它只关心鼠标、指针、键盘和滚轮事件。 不确定拖动事件是否可以克隆。

不幸的是,它在我们的代码库中不可用,因为我们需要 IE11 支持,但也许其他人可以对其进行调整以供自己使用。

是什么让这特别令人难以置信,是“默认”行为再次使组件树_down_冒泡。 取下面的树:

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

我已经折腾了几个小时了,至于为什么我的表单的onSubmit从来没有被调用过这种精确的组件组合——无论我是单击提交按钮还是在表单内的输入字段中按 Enter 键。

最后,我发现这是因为 React Router Link组件有一个onClick实现,它执行e.preventDefault()以防止浏览器重新加载。 然而,这有一个不幸的副作用,即也阻止了点击提交按钮的默认行为,这恰好是提交表单。 所以我今天学到的是 onSubmit 实际上是由浏览器调用的,作为按下提交按钮的默认操作。 即使你按下回车键,它也会触发提交按钮的点击,从而触发表单提交。

但是你会看到事件冒泡顺序如何让这真的很奇怪。

  1. <input> [按键输入]
  2. <button type="submit"> 【模拟点击】
  3. <Menu> [事件在门户外传播]
  4. <Link> [传播到达父级Link ]
  5. <Link> [调用e.preventDefault() ]
  6. => 提交按钮点击的默认浏览器响应被取消
  7. => 表单未提交

即使我们已经在 DOM 中传递了按钮和表单,也会发生这种情况,而Link与它无关,也根本不打算阻止这种行为。

我的解决方案(如果有人遇到同样的问题)是使用onClick={e => e.stopPropagation()}<Menu>内容包装在 div 中的常用解决方案。 但我的观点是我浪费了很多时间来追踪问题,因为这种行为真的很不直观。

我的解决方案(如果有人遇到同样的问题)是使用onClick={e => e.stopPropagation()}<Menu>内容包装在 div 中的常用解决方案。 但我的观点是我浪费了很多时间来追踪问题,因为这种行为真的很不直观。

是的 - 问题的每个_个体实例_都有相同的简单解决方案,_一旦您遇到错误并正确识别它_。 这是 React 团队在这里开辟的一个非常陡峭的失败坑,没有听到他们的任何承认令人沮丧。

我花了几天时间试图调试mouseenter意外冒出门户的另一个问题。 即使在门户的 div 上有onMouseEnter={e => e.stopPropagation()} ,事件仍然冒泡到拥有的 Button,如https://github.com/facebook/react/issues/11387#issuecomment -340009465(第一个对此问题发表评论)。 mouseenter / mouseleave就不应该冒泡...

也许更奇怪的是,当我看到一个mouseenter合成事件从一个按钮的入口冒出来时, e.nativeEvent.typemouseout 。 React 正在触发基于冒泡原生事件的非冒泡合成事件——尽管在合成事件上调用了stopPropagation

@gaearon @trueadm这个问题已经造成两年多来持续的、巨大的挫败感。 该线程是 React 上最活跃的问题之一。 ,可以从球队有人在这里贡献?

在我的情况下,通过单击 Button 打开 Window 组件会使窗口消失,因为单击 Window 导致单击导致状态更改的按钮

我是 React 的新手,我主要使用 jQuery 和 vanilla JS,但这是令人兴奋的错误。 可能有 1% 的情况会出现这种行为......

我喜欢@diegohaz两个解决方案,但我仍然认为createPortal应该有一个选项来阻止事件冒泡。

我的特定用例是工具提示的onMouseLeaveonMouseEnter处理程序由其孩子的门户后代触发——这是不希望的。 本机事件通过忽略门户后代解决了这个问题,因为它们不是 dom 后代。

+1 用于停止在门户中冒泡的选项。 有人建议将门户作为事件侦听器起源的组件的同级(而不是子级)放置,但我认为这在许多用例(包括我的)中不起作用。

最终看起来ReactDOM.unstable_renderSubtreeIntoContainer将被删除,这意味着很快就不会为这个问题留下任何合理的解决方法......

^ 帮助我们@trueadm -nobi 你是我们唯一的希望

看起来在 GitHub 上 ping 它们不起作用 😞
也许拥有活跃 Twitter 帐户的人可以发布有关此内容的推文,标记其中一位贡献者?

为此问题添加我的 +1。 在 Notion,我们目前使用早于React.createPortal的自定义门户实现,我们手动将上下文提供程序转发到新树。 我尝试采用React.createPortal但被意外冒泡行为阻止:

@sebmarkbage建议将<Portal>移到<MenuItem>组件之外以成为兄弟组件,这只能解决单个嵌套级别的问题。 如果您有多个嵌套的(例如)菜单项可以导出子菜单,问题仍然存在。

此问题已自动标记为陈旧。 如果此问题仍然影响您,请留下任何评论(例如,“bump”),我们会保持开放。 很抱歉,我们还不能确定它的优先级。 如果您有任何新的附加信息,请将其包含在您的评论中!

撞。

丹就一个相关问题发表了评论

@mogelbrod我目前没有任何要添加的内容,但是如果您要迁移现有组件,这样的内容( #11387(评论) )对我来说似乎是合理的。

Dan 在同一问题中的跟进

感谢您提供有关解决方法的上下文。 由于您已经拥有该领域的知识,因此最好的下一步可能是为您想要的行为和您考虑的替代方案编写 RFC: https :

无论如何,不支持unstable_renderSubtreeIntoContainer ,所以让我们理清这两个讨论。 我们不会向它添加 Context 的传播,因为整个 API 都被冻结并且不会更新。

我们绝对应该发布一个 React RFC 来建议添加所讨论的标志,或者其他解决方案。 是否有人对起草一份特别感兴趣(也许是@justjake@craigkovatch或 @jquense)? 如果没有,我会看看我能想出什么!

虽然我对发展这个 API 很感兴趣,但我对起草 RFC 没有兴趣。 主要是因为这是一堆工作,几乎不可能被接受,我认为核心团队实际上并没有考虑尚未在其路线图上的 RFC。

@jquense我认为这不准确。 是的,我们不太可能合并不符合愿景的 RFC,因为添加新 API 始终是跨领域的,并且会影响所有其他计划中的功能。 公平地说,我们不经常评论那些不起作用的东西。 但是,我们确实会通读它们,尤其是当我们处理生态系统具有更多专业知识的主题时。例如, https : //github.com/reactjs/rfcs/pull/38、https://github.com/ reactjs/rfcs/pull/150https://github.com/reactjs/rfcs/pull/118https://github.com/reactjs/rfcs/pull/109https://github.com/reactjs/ rfcs/pull/32尽管我们没有明确评论它们,但它们都对我们的思想产生了影响。

换句话说,我们将 RFC 部分视为一种社区研究机制。 来自@mogelbrod (https://github.com/facebook/react/issues/16721#issuecomment-674748100) 的关于为什么解决方法令人讨厌的评论正是我们希望在 RFC 中看到的那种事情。 考虑现有解决方案及其缺点可能比具体的 API 建议提案更有价值。

@gaearon我的评论并不是建议团队不听取外部反馈。 你会做得很好。 我确实认为我的评论是准确的。 _process_ 在 RFC 存储库中执行时不会导致其他人接受 RFC。 看看合并了哪些 RFC,完全是核心团队成员或 fb 员工,没有其他人。 实现它的特性通常有点不同,并且根本不参与 RFC 过程(例如同构 ID)。

我很高兴听到你们会看其他 RFC 并且他们为功能设计做出贡献,但是“我们受到了这些外部 RFC 的影响,即使我们从未对它们发表过评论”,我想说明了我的观点,而不是挑战它。

换句话说,我们将 RFC 部分视为一种社区研究机制。

这是非常合理的,但不是 RFC repo 所说的 _its_ 方法,也不是其他人通常对 RFC 的看法。 RFC 流程通常是团队和社区之间的联系和沟通点,以及在功能考虑和流程方面的公平竞争环境。

除了关于社区治理的更重要的观点。 要求人们花时间写下详细的建议,然后向其他外部参与者捍卫它们,同时反应团队的沉默令人沮丧,并积极强化了 FB 只关心自己对 OSS 的需求的印象。 这很糟糕,因为我知道你们不会那样感觉或设计。

如果 RFC 流程应该是:“您可以在此处概述您的关注点和用例,我们将在何时/如果我们达到能够实施此功能的程度时阅读它们”。 老实说,这是一个很好的方法。 我认为社区将受益于明确说明的内容,否则 ppl 将(并且确实)假设其他 RFC 流程通常具有的参与和参与水平,然后在没有发挥作用时积极劝阻。 即使我比其他贡献者更有洞察力,我当然也有这种印象。

当然,我想我同意所有这些。 我不想把它变成一个元线程,只是说,由于人们一直在关注这个线程,推动这一进程同时关注这两个方面方考虑和编纂问题空间的深刻理解。 我完全理解如果这不是人们愿意接受的事情(部分基于我们对 RFC 的回应),这就是为什么我没有早点提出建议 - 但由于我不断收到个人 ping 我想建议作为有动力的人的选择。

足够公平,这不是在 RFC 上获取元数据的正确位置:)

@gaearon这是当前在 React 上打开的第 6 个投票最多的问题,也是第 4 个评论最多的问题。 它自 React 16 发布以来一直开放,距现在 3 岁仅剩 2 个月。 然而,React 核心团队的参与很少。 在经历了那么多时间和痛苦之后,说“由社区提出解决方案”感觉很不屑一顾。 请注意,虽然它确实有一些非常有用的应用程序,但这种默认行为是设计错误。 社区不应该由 RFC 来修复它。

我很遗憾在这个问题上发表评论,并收回我对社区 RFC 的建议。 你说得对,这可能是个坏主意。 我必须补充一点,这个问题已经变得非常情绪化,作为一个人,我个人觉得很难参与其中——尽管我明白这很重要,而且很多人对此有强烈的感受。

让我简单地回答一下这个线程的状态。

首先,我想向那些发表评论并因我们没有继续跟进此主题而感到沮丧的人道歉。 如果我从外面看这个问题,我的印象可能是 React 团队犯了一个错误,不愿意承认,并且愿意坐在一个简单的解决方案上(“只需添加一个布尔值,有多难?它是!”)两年多,因为他们不关心社区。 我完全可以理解你是如何得出这个结论的。

我知道这个问题被高度赞成。 这个问题在这个帖子里已经多次提到了,也许从这个角度来看,如果 React 团队知道这是一个很大的痛点,我们会早点解决它。 我们知道这是一个痛点——人们经常私下给我们发消息,或者把它作为 React 团队不关心社区的一个例子。 虽然我完全承认沉默令人沮丧,但“只是做某事”的压力越来越大,这使得更难以有效地解决这个问题。

这个问题有解决方法——这使它不像安全漏洞或必须紧急处理的崩溃。 我们知道 wokarounds 有效(但并不理想并且可能很烦人),因为我们自己使用了其中的一些,尤其是在 React 16 之前编写的代码周围。我认为我们可以同意,虽然这个问题无疑让很多人感到沮丧人数,它仍然属于不同类别的问题,而不是必须在具体时间范围内应对的崩溃或安全​​问题。

此外,我不同意我们明天可以实施的简单解决方案的框架。 即使我们认为最初的行为是一个错误(我不确定我是否同意),这意味着让下一个行为处理各种用例的门槛更高。 如果我们修复了一些案例但破坏了其他案例,我们就没有取得任何进展,并且造成了大量的流失。 请记住,我们不会听到当前行为在此问题中运行良好的情况。 我们只有在打破它后才会听到它。

举个例子,当前的行为实际上对我们已经研究了很长时间的声明式焦点管理用例非常有帮助。 尽管它是一个 Portal,但将焦点/模糊视为发生在与部件树有关的模态“内部”是有用的。 如果我们要发布此线程中建议的“简单” createPortal(tree, boolean)提案,则此用例将不起作用,因为门户本身无法“知道”我们想要哪种行为。 任何对可能解决方案的探索都需要考虑数十个用例,其中一些甚至尚未完全理解。 这在某些时候肯定是必要的,但它也是一个巨大的时间承诺来做正确的,到目前为止我们还没有能够专注于它。

活动尤其是一个棘手的领域,例如,我们刚刚进行了一系列更改以解决多年来存在的问题,而这一直是今年的重点。 但是我们一次只能做这么多事情。

一般来说,我们作为一个团队,尽量深入地关注少数问题,而不是肤浅地关注许多问题。 不幸的是,这确实意味着一些概念上的缺陷和差距可能多年无法得到填补,因为我们正在修复其他重要的差距,或者没有制定出可以彻底解决问题的替代设计。 我知道听到这令人沮丧,这也是我远离这个线程的部分原因。 其他一些类似的帖子已经变成了对问题和可能的解决方案的更深入的解释,这很有帮助,但是这个帖子大多变成了大量的“+1”和对“简单”修复的建议,这就是为什么它很难有意义地参与其中。

我知道这不是人们想听到的答案,但我希望这总比没有答案好。

另一件值得一提的事情是,该线程中描述的一些痛点可能已经通过其他方式解决了。 例如:

具体来说:在合成焦点事件上调用 stopPropagation 以防止它从门户中冒泡会导致 stopPropagation 在 React 捕获的#document 处理程序中的本地焦点事件上也被调用,这意味着它没有将其发送到另一个捕获的处理程序

React 不再使用捕获阶段来模拟冒泡,也不再监听文档上的事件。 因此,在不消除沮丧的情况下,绝对有必要根据其他更改重新评估迄今为止发布的所有内容。

本机事件仍然冒泡,而且由于我们的 React 代码托管在一个主要是 jQuery 的应用程序中,全局 jQuery keyDown 处理程序在

仍然得到事件。

类似地,React 17 会将事件附加到根和门户容器(并在那时实际上停止本机传播),所以我也希望得到解决。

关于renderSubtreeIntoContainer被删除的点。 从字面上看,它与ReactDOM.render唯一区别在于它传播 Legacy Context。 由于任何不包含renderSubtreeIntoContainer也不包含 Legacy Context,因此ReactDOM.render将保持 100% 相同的替代方案。 当然,这并不能解决更广泛的问题,但我认为对renderSubtree的关注有些不妥。

@gaearon

关于renderSubtreeIntoContainer被删除的点。 从字面上看,它与ReactDOM.render唯一区别在于它传播 Legacy Context。 由于任何不包含renderSubtreeIntoContainer也不包含 Legacy Context,因此ReactDOM.render将保持 100% 相同的替代方案。 当然,这并不能解决更广泛的问题,但我认为对renderSubtree的关注有些不妥。

既然你提到了它,我想知道下面的代码是否是一个没有事件冒泡的 React Portal 的有效和安全的实现:

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: https ://codesandbox.io/s/react-portal-with-reactdom-render-m22dj?file

仍然存在不通过 Modern Context 的问题,但这不是一个新问题( renderSubtree也受到它的影响)。 解决方法是用一堆上下文提供程序围绕您的树。 总的来说,嵌套树并不理想,所以我不建议在遗留现有代码场景之外的任何情况下转向这种模式。

再一次,非常感谢你写的@gaearon!

对于核心团队之外的人来说,聚合损坏案例列表 + 解决方法(针对 React v17 更新)听起来似乎是最有成效的事情(如果我错了,请纠正我!)。

接下来的几周我很忙,但我的目标是尽快做。 如果其他人能够更早地做到这一点,或者使用片段(就像@diegohaz刚刚做的那样),那就太棒了!

汇总案例列表肯定会很有用,尽管我认为它不仅需要包括损坏的案例,还需要包括当前行为有意义的案例。

如果有公共空间可以添加,我很乐意添加来自我们的应用程序和 UI 库作者的用例。 总的来说,我同意丹的观点,虽然有时很烦人,但很容易解决。 对于您确实希望 React 冒泡的情况,如果没有 React 的帮助,很难覆盖这种情况。

汇总案例列表肯定会很有用,尽管我认为它不仅需要包括损坏的案例,还需要包括当前行为有意义的案例。

如果有人可以向我指出一些依赖于它的开源代码/提取代码,我很乐意包含这些内容! 就像你之前提到的,找到这个有点困难,因为只有对当前行为有问题的人才会参与这个问题😅

如果有公共空间可以添加,我很乐意添加来自我们的应用程序和 UI 库作者的用例。 总的来说,我同意丹的观点,虽然有时很烦人,但很容易解决。 对于您确实希望 React 冒泡的情况,如果没有 React 的帮助,很难覆盖这种情况。

您有任何特定的空间,或者每个案例共享一个codeandbox (或 jsfiddle 等)作为初学者? 一旦我们收集了一些案例,我可以尝试编译它们。

我在这里开始了一个线程: https :

此页面是否有帮助?
0 / 5 - 0 等级