React: createPortal: opção de suporte para interromper a propagação de eventos na árvore React

Criado em 27 out. 2017  ·  103Comentários  ·  Fonte: facebook/react

Você quer solicitar um recurso ou relatar um bug ?
Recurso, mas também causa um bug que quebra a velha API unstable_rendersubtreeintocontainer

Qual é o comportamento atual?
Não podemos parar a propagação de todos os eventos do portal para seus ancestrais da árvore React. Nosso mecanismo de camadas com modais / popovers completamente quebrados. Por exemplo, temos um botão suspenso. Quando clicamos nele, o clique abre o popover. Também queremos fechar este popover ao clicar no mesmo botão. Com createPortal, clique dentro do popover dispara clique no botão e está fechando. Podemos usar stopPropagation neste caso simples. Mas temos toneladas desses casos e precisamos usar stopPropagation para todos eles. Além disso, não podemos parar todos os eventos.

Qual é o comportamento esperado?
O createPortal deve ter uma opção para interromper a propagação de eventos sintéticos por meio da árvore React sem interromper manualmente cada evento. O que você acha?

DOM Feature Request

Comentários muito úteis

Mesmo isso parece desnecessariamente complexo para mim. Por que não simplesmente adicionar um sinalizador booleano opcional para createPortal, permitindo que o comportamento de bolha seja bloqueado?

Todos 103 comentários

Além disso, a propagação de mouseOver / Leave parece completamente inesperada.
image

Você pode mover o portal para fora do botão?

por exemplo

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

Então, não borbulhará através do botão.

Foi meu primeiro pensamento, mas!) Imagine, que temos o manipulador mouseEnter em tal contêiner de componente:

image

Com unstable_rendersubtreeintocontainer eu não preciso fazer nada com eventos no componente ButtonWithPopover - mouseEnter simplesmente funciona quando o mouse realmente entra em div e o elemento DOM do botão, e não é acionado quando o mouse está sobre o popover. Com o portal, o evento dispara ao passar o mouse sobre o popover - e, na verdade, NÃO sobre div neste momento. Então, eu preciso parar a propagação. Se eu fizer isso no componente ButtonWithPopover , interromperei o disparo do evento quando o mouse estiver sobre o botão. Se eu fizer isso no popover e estiver usando algum componente popover comum para este aplicativo, também posso interromper a lógica em outras partes do aplicativo.

Eu realmente não entendo o propósito de borbulhar na árvore React. Se eu precisar de eventos do componente do portal - simplesmente posso passar os manipuladores por adereços. Fomos fazer isso com unstable_rendersubtreeintocontainer e funcionou perfeitamente.

Se eu abrir uma janela modal a partir de algum botão nas profundezas da árvore react, receberei disparos inesperados de eventos no modal. stopPropagation também interromperá a propagação no DOM e não receberei eventos que realmente espero que sejam acionados (

@gaearon Eu sugeriria que isso é mais um bug do que uma solicitação de recurso. Temos uma série de novos bugs causados ​​por eventos de mouse que borbulham nos portais (onde antes usávamos unstable_rendersubtreeintocontainer ). Alguns deles não podem ser corrigidos mesmo com uma camada div extra para filtrar os eventos do mouse porque, por exemplo, contamos com a propagação de eventos do mouse para o documento para implementar diálogos arrastáveis.

Existe uma maneira de contornar isso antes que isso seja tratado em uma versão futura?

Acho que está sendo chamado de solicitação de recurso, porque o comportamento atual da bolha dos portais é esperado e intencional. O objetivo é que a subárvore aja como um filho real de seus pais.

O que seria útil são casos de uso ou situações adicionais (como os que você está vendo) que você acha que não são atendidos pela implementação atual ou são difíceis de contornar

Eu entendo que esse comportamento é intencional, mas acho que é um bug significativo que não pode ser desativado.

Em minha opinião, a biblioteca que trabalha com DOM deve preservar o comportamento de implementação do DOM e não quebrá-lo.

Por exemplo:

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

Quando trabalho com o DOM, espero receber eventos como a implementação do DOM. No meu exemplo, os eventos são propagados através do Portal , contornando seus pais DOM, e isso pode ser considerado um bug .

Pessoal, obrigado pela discussão, no entanto, não acho que seja muito útil discutir se algo é um bug ou não. Em vez disso, seria mais produtivo discutir os casos de uso e exemplos que não são atendidos pelo comportamento atual, para que possamos entender melhor se o jeito atual é o melhor para o futuro.

Em geral, queremos que a API lide com um conjunto diversificado de casos de uso, ao mesmo tempo que esperamos não limitar demais os outros. Não posso falar pela equipe principal, mas imagino que torná-la configurável não é uma solução provável. Geralmente, o React se inclina para uma API consistente sobre as configuráveis.

Eu também entendo que esse comportamento não é como o DOM funciona, mas não acho que seja um bom motivo para dizer que não deveria ser assim. Muito do comportamento do react-dom é diferente de como o DOM funciona, muitos eventos já são diferentes da versão nativa. onChange por exemplo, é completamente diferente do evento de mudança nativo, e todos os eventos reac borbulham independentemente do tipo, ao contrário do DOM.

Em vez disso, seria mais produtivo discutir os casos de uso e exemplos que não são atendidos pelo comportamento atual

Aqui estão dois exemplos que foram quebrados para nós em nossa migração para React 16.

Primeiro, temos uma caixa de diálogo arrastável que é iniciada por um botão. Tentei adicionar um elemento de "filtragem" em nosso uso do Portal, que chamou StopPropagation em qualquer evento de mouse * an key *. No entanto, contamos com a capacidade de vincular um evento mousemove ao documento para implementar a funcionalidade de arrastar - isso é comum porque se o usuário mover o mouse a qualquer taxa significativa, o cursor sai dos limites da caixa de diálogo e você precisa para ser capaz de capturar o movimento do mouse em um nível superior. Filtrar esses eventos quebra essa funcionalidade. Mas com os Portals, os eventos do mouse e da tecla estão surgindo de dentro da caixa de diálogo para o botão que a lançou, fazendo com que ele exiba diferentes efeitos visuais e até mesmo dispense a caixa de diálogo. Não acho que seja realista esperar que cada componente que será iniciado por meio de um Portal vincule manipuladores de evento de 10 a 20 para interromper a propagação do evento.

Em segundo lugar, temos um menu de contexto pop-up que pode ser iniciado por um clique primário ou secundário do mouse. Um dos consumidores internos de nossa biblioteca possui manipuladores de mouse anexados ao elemento que inicia este menu e, claro, o menu também possui manipuladores de clique para manipular a seleção de itens. O menu agora reaparece a cada clique, à medida que os eventos mousedown / mousedown estão retornando ao botão que abre o menu.

Não posso falar pela equipe principal, mas imagino que torná-la configurável não é uma solução provável. Geralmente, o React se inclina para uma API consistente sobre as configuráveis.

Imploro a você (e à equipe) que reconsiderem esta posição neste caso específico. Acho que o borbulhamento de eventos será interessante para certos casos de uso (embora eu não consiga pensar em nenhum improvisado). Mas acho que será paralisante em outros e introduz uma inconsistência significativa na API. Embora unstable_rendersubtreeintocontainer nunca tenha sido super-suportado, era o que todos costumavam renderizar fora da árvore imediata e não funcionava dessa forma. Ele foi oficialmente descontinuado em favor dos Portals, mas os Portals quebram a funcionalidade dessa forma crítica e não parece ser uma solução alternativa fácil. Acho que isso pode ser descrito como bastante inconsistente.

Eu também entendo que esse comportamento não é como o DOM funciona, mas não acho que seja um bom motivo para dizer que não deveria ser assim.

Eu entendo de onde você está vindo, mas acho que neste caso (a) é um comportamento fundamental que (b) atualmente não tem solução alternativa, então eu acho que "o DOM não funciona dessa maneira" é um argumento forte, se não for completamente convincente.

E para ser claro: meu pedido para que isso seja considerado um bug é principalmente para que ele seja priorizado para uma correção mais cedo ou mais tarde.

Meu modelo mental de Portal é que ele se comporta como se estivesse no mesmo lugar da árvore, mas evita problemas como "estouro: oculto" e evita rolar para fins de desenho / layout.

Existem muitas soluções "popup" semelhantes que acontecem inline sem um Portal. Por exemplo, um botão que expande uma caixa ao lado dele.

Veja como exemplo a caixa de diálogo "Escolha sua reação" aqui no GitHub. Isso é implementado como um div ao lado do botão. Isso funciona bem agora. No entanto, se quiser ter um Z-index diferente ou ser retirado de uma área overflow: scroll que contém esses comentários, será necessário alterar a posição do DOM. Essa mudança não é segura, a menos que outras coisas, como o borbulhamento de eventos, também sejam preservadas.

Ambos os estilos de "pop-ups" ou "pop-outs" são legítimos. Então, como você resolveria o mesmo problema quando o componente está embutido no layout em vez de flutuando fora dele?

A solução alternativa que funcionou para mim foi chamar stopPropagation diretamente na renderização do portal:

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

Isso funciona muito bem para mim, já que tenho um único componente de abstração que usa portais, caso contrário, você precisará consertar todas as suas chamadas de createPortal .

@methyl, pressupõe que você conhece todos os eventos que precisa para impedir que borbulhem na árvore. E no caso que mencionei com diálogos arrastáveis, precisamos de mousemove para borbulhar para o documento, mas não para borbulhar na árvore de renderização.

Ambos os estilos de "pop-ups" ou "pop-outs" são legítimos. Então, como você resolveria o mesmo problema quando o componente está embutido no layout em vez de flutuando fora dele?

@sebmarkbage Não tenho certeza se esta pergunta faz sentido. Se eu tivesse esse problema ao embutir o componente, não o embutiria.

Acho que alguns dos problemas aqui são alguns casos de uso de renderSubtreeIntoContainer estão sendo transferidos para createPortal quando os dois métodos estão fazendo coisas conceitualmente diferentes. O conceito de Portal estava sendo sobrecarregado, eu acho.

Concordo que, no caso de diálogo Modal, você quase nunca deseja que o modal aja como um filho do botão que o abriu. O componente acionador só o está renderizando porque controla o estado open . Acho que é um erro dizer que a implementação do portal está, portanto, errada, em vez de dizer que createPortal no botão não é a ferramenta certa para isso. Nesse caso, o Modal não é filho da trigger e não deve ser renderizado como se fosse. Uma solução possível é continuar usando renderSubtreeIntoContainer , outra opção de usuário é ter um ModalProvider perto da raiz do aplicativo que lida com os modais de renderização e transmite (por meio do contexto) um método para renderizar um elemento modal arbitrário precisa para a raiz

renderSubtreeIntoContainer não pode ser chamado de dentro de render ou métodos de ciclo de vida no React 16, o que praticamente impede seu uso para os casos que venho discutindo (na verdade, todos os nossos componentes que foram fazer isso quebrou completamente na migração para 16). Portals são a recomendação oficial: https://reactjs.org/blog/2017/09/26/react-v16.0.html#breaking -changes

Concordo que o conceito de Portals pode ter ficado sobrecarregado. Não tenho certeza se amo a solução de um componente global e contexto para isso, no entanto. Parece que isso pode ser facilmente resolvido por um sinalizador em createPortal especificando se os eventos devem aparecer. Seria um sinalizador opcional que preservaria a compatibilidade da API com 16+.

Tentarei esclarecer nosso caso de uso dos portais e por que adoraríamos ver uma opção para interromper a propagação de eventos. No aplicativo ManyChat, estamos usando portais para criar 'camadas'. Temos o sistema de camadas para todo o aplicativo que é usado por vários tipos de componentes: popovers, dropdowns, menus, modais. Cada camada pode expor uma nova camada, por exemplo, o botão em um segundo nível do menu pode acionar a janela modal onde o outro botão pode abrir o popover. Na maioria dos casos, a camada é o novo ramo da UX que resolve sua própria tarefa. E quando a nova camada é aberta, o usuário deve interagir com esta nova camada, não com as outras na parte inferior. Portanto, para este sistema, criamos um componente comum para renderizar em camadas:

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

Este componente interrompe a propagação de todos os tipos de eventos dos documentos do React e nos permitiu atualizar para o React 16.

Isso precisa estar vinculado a portais? Em vez de portais de sandbox, e se houvesse apenas um (por exemplo) <React.Sandbox>...</React.Sandbox> ?

Mesmo isso parece desnecessariamente complexo para mim. Por que não simplesmente adicionar um sinalizador booleano opcional para createPortal, permitindo que o comportamento de bolha seja bloqueado?

@gaearon, esta é uma situação bastante infeliz para uma parte de nós - você ou alguém querido poderia dar uma olhada nisso? :)

Eu acrescentaria que meu pensamento atual é que ambos os casos de uso devem ser suportados. Existem realmente casos de uso em que você precisa que o contexto flua do pai atual para a subárvore, mas para que essa subárvore não atue como um filho lógico em termos do DOM. Modais complexos são o melhor exemplo, você quase nunca quer que os eventos de um formulário em uma janela modal se propaguem até o botão de disparo, mas quase certamente precisa que o contexto seja passado (i18n, temas, etc)

Direi que esse caso de uso _pode_ ser resolvido principalmente com um ModalProvider mais perto da raiz do aplicativo que renderiza via createPortal alto o suficiente para que a propagação do evento não afete nada, mas isso começa a parecer uma solução alternativa ao invés de arquitetura bem projetada. Também torna os modais fornecidos pela biblioteca mais irritantes para os usuários, uma vez que não são mais independentes.

Eu acrescentaria que, em termos de API, não acho que createPortal deva fazer as duas coisas, o caso modal realmente deseja apenas usar ReactDOM.render (antigo skool) porque é muito próximo de uma árvore distinta _except_ que a propagação do contexto é freqüentemente necessária

Nós apenas tivemos que corrigir um bug extremamente difícil de diagnosticar no código de gerenciamento de foco do nosso aplicativo externo como resultado do uso da solução alternativa que @ kib357 postou.

Especificamente: chamar stopPropagation no evento de focus sintético para evitar que ele borbulhe para fora do portal faz com que stopPropagation também seja chamado no evento de focus nativo no manipulador capturado do React em #document, o que significa que não chegou a outro manipulador capturado em <body> . Corrigimos movendo nosso manipulador para #document, mas evitamos especificamente fazer isso no passado para não pisar nos dedos do React.

O novo comportamento borbulhante nos Portals realmente parece um caso minoritário para mim. Seja essa opinião ou verdade, podemos, por favor, obter um pouco de tração sobre esse assunto? Talvez @gaearon? Tem quatro meses e está causando muita dor. Acho que isso poderia ser descrito como um bug, visto que é uma alteração de API interrompida no React 16, sem nenhuma solução alternativa totalmente segura.

@craigkovatch Ainda estou curioso para saber como você resolveria meu exemplo embutido. Digamos que o pop-up esteja empurrando o tamanho da caixa para baixo. Inlining algo é importante, pois é empurrar algo para baixo no layout devido ao seu tamanho. Não pode simplesmente pairar sobre.

Você poderia medir o popover, inserir um espaço reservado em branco com o mesmo tamanho e tentar alinhá-lo na parte superior, mas não é isso que as pessoas fazem.

Portanto, se seu popover precisar expandir o conteúdo local, como ao lado do botão, como você resolveria? Suspeito que o padrão que funciona lá funcionará em ambos os casos e devemos recomendar apenas um padrão.

Acho que, em geral, esse é o padrão que funciona em ambos os cenários:

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

Se o Flyout for um portal, ele funciona e não recebe eventos de mouse over ao passar o mouse sobre o portal. Mas, o mais importante, ele também funciona se NÃO for um portal e precisa ser um menu desdobrável embutido. Não é necessário stopPropagation.

Então, o que há nesse padrão que não funciona para o seu caso de uso?

@sebmarkbage , estamos usando Portals de uma maneira completamente diferente, renderizando em um contêiner montado como o filho final de <body> que é então posicionado, às vezes com um Z-index. A documentação do React sugere que isso está mais próximo da intenção do projeto; ou seja, renderizando em um local totalmente diferente no DOM. Não me parece que nossos casos de uso sejam semelhantes o suficiente para que a discussão pertença a este tópico. Mas se vocês quiserem fazer um brainstorm / solucionar problemas juntos, ficarei mais do que feliz em discutir mais detalhes em outro fórum.

Não, meu caso de uso é ambos . Às vezes um e às vezes o outro. É por isso que é relevante.

O <Flyout /> pode optar por renderizar no filho final do corpo ou não, mas contanto que você içar o próprio portal para um irmão do componente suspenso em vez de um filho dele, seu cenário funcionará.

Eu acho que há um cenário plausível onde isso é inconveniente e você quer uma maneira de teletransportar coisas de componentes profundamente aninhados, mas nesse cenário você provavelmente está bem com o contexto sendo o contexto do ponto intermediário. Mas penso nisso como duas questões distintas.

Talvez precisemos de uma API de slots para isso.

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

O portal obteria então o contexto de Bar, não DeepComponent. O contexto e o borbulhamento de eventos ainda compartilham o mesmo caminho de árvore.

@sebmarkbage o caso modal geralmente requer contexto a partir do ponto em que é renderizado. Acho que é um caso ligeiramente único, o componente é um filho lógico da coisa que o renderizou, mas _não_ estrutural (por falta de uma palavra melhor), por exemplo, você geralmente quer coisas como contexto de formulário (relé, formik, forma redux , tanto faz), mas não eventos DOM para passar. Também se acaba renderizando tais modais bem no fundo das árvores, ao lado de seus gatilhos, para que permaneçam componentes e reutilizáveis, mais do que porque pertencem estruturalmente a eles.

Acho que esse caso é geralmente diferente do caso flyout / dropdown que o createPortal serve. Tbc acho que o comportamento de borbulhamento dos portais é bom, mas não para os modais. Eu também acho que isso poderia funcionar com Context e algum tipo de ModalProvider razoavelmente bem, mas isso é um tanto chato, especialmente para bibliotecas.

contanto que você içar o próprio portal para um irmão do componente pairado em vez de um filho dele, seu cenário funcionará.

Não tenho certeza se estou entendendo. Ainda há o problema de, por exemplo, eventos keyDown borbulhando em uma árvore DOM inesperada.

@jquense Observe que, em meu exemplo, o slot ainda está dentro do componente Bar, de modo que obteria seu contexto do formulário em algo como <Form><Bar /></Form> .

Mesmo que o portal seja processado no corpo do documento.

Portanto, é como dois caminhos indiretos (portais): profundo -> irmão de Bar -> corpo do documento.

Portanto, o contexto do portal ainda é o contexto do formulário, assim como a cadeia borbulhante do evento, mas também não está no contexto da coisa pairada.

Sim, desculpe, perdi isso 😳 Se eu estou lendo direito, você ainda estaria borbulhando <Slot> cima? Isso é definitivamente melhor, embora eu ache que no caso de diálogo Modal provavelmente não se queira _qualquer_ borbulhamento. Como pensar em termos de um leitor de tela, você deseja que tudo fora do modal seja invertido, enquanto ele está ativo. Não sei, acho que, nesse caso, o borbulhar é um problema, ninguém esperaria que um clique dentro de uma caixa de diálogo borbulhasse em qualquer lugar.

Talvez o problema aqui não sejam portais, mas não há uma boa maneira de compartilhar Contexto entre árvores? Uma parte da coisa do contexto ReactDOM.render é realmente bom para modais, e talvez uma maneira mais "correta" de pensar sobre isso de qualquer maneira ...

Meu pensamento aqui é que há algum borbulhamento porque ainda vai do modal para o div, para o corpo, para o documento e para a janela. E conceitualmente além da moldura, para a janela que o contém e assim por diante.

Isso não é teórico em algo como conteúdo renderizado ART ou GL (e, até certo ponto, React Native), onde pode não haver uma árvore de apoio existente para obter essa semântica. Então, é preciso haver uma maneira de dizer que este é o lugar onde bolhas.

Em alguns aplicativos, existem modais em modais. Por exemplo, no FB há uma janela de bate-papo que pode estar acima de um modal ou um modal pode fazer parte da janela de bate-papo. Portanto, mesmo um modal tem algum contexto de onde na árvore ele pertence. Nunca é totalmente autônomo.

Isso não quer dizer que não possamos ter duas semânticas diferentes para borbulhamento de eventos e contexto. Isso é explícito sobre isso e você pode criar um portal sem o outro, etc.

Ter a garantia de que ambos seguem o mesmo caminho é realmente poderoso, pois significa que o borbulhamento de eventos pode ser totalmente implementado para eventos de espaço do usuário da mesma forma que no navegador.

Por exemplo, isso acontece com vários contextos Redux hoje. Imagine que this.context.dispatch("Hover") é um evento do espaço do usuário borbulhando. Podemos até implementar eventos React como parte do contexto. Parece razoável pensar que posso usar isso da mesma maneira, e de todas as maneiras agora, você pode. Acho que se bifurcássemos esses dois contextos, provavelmente acabaríamos com outra API de contexto de espaço do usuário que segue a estrutura DOM em paralelo com o contexto normal - se eles forem realmente tão diferentes.

Então é por isso que estou forçando um pouco contra isso para ver se a coisa dos slots pode ser suficiente, uma vez que a) você precisa ser explícito sobre qual borbulhamento de contexto acontece de qualquer maneira. b) pode evitar a bifurcação do mundo e ter dois sistemas de contexto inteiros.

Especificamente: chamar stopPropagation no evento de focus sintético para evitar que borbulhe para fora do portal faz com que stopPropagation também seja chamado no evento de focus nativo no manipulador capturado do React em #document, o que significa que não foi para outro manipulador capturado em

. Corrigimos movendo nosso manipulador para #document, mas evitamos especificamente fazer isso no passado para não pisar nos dedos do React.

@craigkovatch , você usou o evento onFocusCapture no documento? Na minha solução alternativa, os eventos capturados não devem ser interrompidos. Você pode fornecer um exemplo mais detalhado de como foi e o que você fez para resolver seu problema?
Além disso, acho que meu código tem um problema com a interrupção do evento blur - não deve ser interrompido. Então, vou investigar esta questão mais profundamente e tentar encontrar uma solução mais confiável.

@ kib357 Não estou sugerindo que haja um problema em sua solução alternativa, acho que há um bug separado no React lá (ou seja, não deve cancelar a propagação de eventos de focus nativos na fase de captura ao chamar stopPropagation em eventos de focus sintéticos na fase de bolhas).

O código em questão usa um ouvinte de evento de captura nativa, ou seja, document.body.addEventListener('focus', handler, true)

@craigkovatch parece interessante, dado o fato de que você usou o manipulador capturado. No entanto, não tenho nenhuma ideia do porquê isso acontece.

Então, pessoal, temos dois cenários diferentes para usar a renderização do portal:

  1. Para evitar problemas de CSS como overflow: hidden e etc. em widgets simples, como botões suspensos ou menus de um nível
  2. Para criar uma nova camada de UX para casos mais poderosos como:
  3. modais
  4. menus aninhados
  5. popovers-with-forms-with-dropdowns -... - todos os casos, quando as camadas são combinadas

Acho que a createPortal API atual satisfaz apenas o primeiro cenário. A sugestão de usar um novo React.render para o segundo é inutilizável - é muito ruim criar um aplicativo separado com todos os provedores de TI para cada camada.
Que informações adicionais podemos fornecer para ajudar a resolver esse problema?
Quais são as desvantagens do parâmetro sugerido na API createPortal ?

@sebmarkbage Minha pergunta imediata com a API de slots é: eu seria capaz de inserir vários SlotContents em um Slot ao mesmo tempo? Não é incomum em nossa interface ter vários "popups" ou "modais" abertos simultaneamente. No meu mundo perfeito, uma API Popup seria algo assim:

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

Temos um novo problema para o qual não consegui encontrar uma solução alternativa. Usando a abordagem de "armadilha de evento" sugerida acima, apenas eventos React Synthetic são impedidos de borbulhar para fora do portal. Os eventos nativos ainda borbulham e, como nosso código React está hospedado em um aplicativo principalmente jQuery, o manipulador global jQuery keyDown em <body> ainda obtém o evento.

Tentei adicionar um ouvinte event.stopPropagation ao elemento de contêiner nativo dentro do Portal por meio de um ref como este, mas isso neutraliza completamente todos os eventos sintéticos dentro do portal - presumi incorretamente que o ouvinte de nível superior do React estava observando a fase de captura.

Não tenho certeza do que pode ser feito aqui, além de alterações no 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>;
}

Isso depende da ordem em que ReactDOM e JQuery são inicializados. Se o JQuery inicializar primeiro, os manipuladores de eventos de nível superior do JQuery serão instalados primeiro e, portanto, serão executados antes que os manipuladores sintéticos do ReactDOM sejam executados.

Tanto o ReactDOM quanto o JQuery preferem ter apenas um ouvinte de nível superior que simula o borbulhamento internamente, a menos que haja algum evento em que o navegador não borbulhe, como scroll .

@Kovensky, segundo meu entendimento, era que jQuery não fazia "bolhas sintéticas" como o React faz e, portanto, não tem um único ouvinte de nível superior. Meu inspetor DOM também não revela nenhum. Adoraria ver o que você está se referindo, se me engano.

Esse será o caso para eventos delegados. Por exemplo, $(document.body).on('click', '.my-selector', e => e.stopPropagation()) .

Olha, isso pode ser resolvido no React, se alguém simplesmente me convencer que isso não pode ser resolvido com meu design proposto acima que requer alguma reestruturação do seu código. Mas eu não vi nenhum motivo que não possa ser feito a não ser apenas tentar encontrar uma solução alternativa rápida.

@sebmarkbage sua proposta resolve apenas o caso de propagação de eventos para o proprietário imediato. E quanto ao resto da árvore?

Aqui está um caso de uso que acho que não pode ser resolvido bem com Slots ou createPortal

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

E aqui está um gif com uma configuração semelhante, mas um pouco diferente, onde estou usando createPortal para um site responsivo, para mover um campo de formulário para a barra de ferramentas do aplicativo (muito mais acima na árvore). Nesse caso também, eu realmente não quero que os eventos voltem ao conteúdo da página, mas definitivamente quero que o contexto do Form acompanhe isso. Minha implementação, aliás, é algo parecido com Slot usando contexto ...

large gif 640x320

@sebmarkbage unstable_renderSubtreeIntoContainer permitiu acesso direto ao topo da hierarquia, independentemente da posição de um componente, seja dentro da hierarquia ou como parte de uma estrutura empacotada separada.

Comparativamente, vejo alguns problemas com a solução de Slot:

  • A solução pressupõe que você tenha acesso à posição da hierarquia onde está "ok" para eventos de bolha. Definitivamente, esse não é o caso de componentes e estruturas de componentes.
  • Presume que é "ok" fazer bolhas de eventos em qualquer outro nível da hierarquia.
  • Os eventos ainda irão surgir a partir da posição do Slot. (como @craigkovatch mencionado)

Eu também tenho um caso de uso (provavelmente semelhante aos já mencionados).

Eu tenho uma superfície onde os usuários podem selecionar coisas com o mouse com um "laço". Isso é basicamente 100% largura / altura e está na raiz do meu aplicativo e usa onMouseDown evento. Nesta superfície também existem botões que abrem portais como modais e suspensos. Um evento mouseDown dentro do portal é, na verdade, interceptado pelo componente de seleção de laço na raiz do aplicativo.

Vejo muitos para resolver o problema:

  • renderizar o portal um passo acima do componente laço raiz, mas isso não é muito conveniente e provavelmente precisaria recorrer a uma biblioteca baseada em contexto como o react-gateway? (ou talvez o sistema de slots mencionado).
  • interromper a propagação manualmente dentro da raiz do portal, mas pode levar aos efeitos colaterais indesejados mencionados acima
  • capacidade de interromper a propagação em portais React (+1 btw)
  • filtrar eventos quando eles vierem de um portal

Por enquanto, minha solução é filtrar os eventos.

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

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


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

Esta claramente não será a melhor solução para todos nós e não será muito legal se você tiver portais aninhados, mas para o meu caso de uso atual (que é o único atualmente) funciona. Não quero adicionar uma nova biblioteca de contexto ou fazer uma refatoração complexa para resolver isso. Só queria compartilhar minha solução.

Consegui realizar o bloqueio de bolhas de eventos, conforme observado em outro lugar neste tópico.

Mas outro problema aparentemente mais espinhoso que estou encontrando é o onMouseEnter SyntheticEvent, que não borbulha. Em vez disso, passa do pai comum do componente from para o componente to , conforme descrito aqui . Isso significa que se o ponteiro do mouse entrar de fora da janela do navegador, cada manipulador onMouseEnter do topo do DOM até o componente em createPortal será acionado nessa ordem, fazendo com que todos os tipos de eventos disparem aquele nunca fez com unstable_renderSubtreeIntoContainer . Já que onMouseEnter não borbulha, não pode ser bloqueado no nível do Portal. (Isso não parecia um problema com unstable_renderSubtreeIntoContainer pois o evento onMouseEnter não respeitava a hierarquia virtual e não se sequenciava pelo conteúdo do corpo, em vez disso descia diretamente para a subárvore.)

Se alguém tiver alguma ideia sobre como evitar que eventos onMouseEnter propaguem do topo da hierarquia DOM ou desviem diretamente para a subárvore do portal, por favor me avise.

@JasonGore também observei esse comportamento.

Por exemplo.

Eu tenho um menu de contexto que é renderizado quando um div é acionado noMouseOver e, em seguida, abro um Modal com createPortal clicando em um dos itens no menu. Quando eu tiro o mouse da janela do navegador, o evento onMouseLeave se propaga até o menu de contexto, fechando o menu de contexto (e, portanto, o Modal) ...

Tive o mesmo problema em que tinha um item de lista que queria que fosse totalmente clicável (como um link), mas queria um botão de exclusão nos rótulos abaixo do nome que abriria um modal para confirmar.

screenshot 2018-10-31 at 11 42 47

Minha única solução era evitar borbulhar na div modal assim:

// components/Modal.js

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

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

Isso vai evitar borbulhar em todos os modais sim, mas não tenho nenhum caso em que eu gostaria que isso acontecesse, então funciona para mim.

Existem problemas potenciais com esta abordagem?

@jnsandrew não se esqueça de que existem cerca de 50 outros tipos de eventos 🙃

Apenas acerte isso. Parece estranho para mim que o React se comporte de uma maneira diferente do borbulhamento de eventos DOM.

+1 para isso. Estamos usando React.createPortal para renderizar dentro do iframe (tanto para estilos quanto para isolamento de eventos) e não poder evitar que os eventos borbulhem fora da caixa é uma chatice.

Parece que este é o 12º problema mais manuseado na lista de pendências do React. Pelo menos os documentos estão abertos sobre isso https://reactjs.org/docs/portals.html#event -bubbling-through-portals - embora eles não mencionem as desvantagens ou soluções alternativas e, em vez disso, observam que permite "abstrações mais flexíveis ":

Os documentos devem pelo menos explicar que isso pode causar problemas e sugerir soluções alternativas. No meu caso, é um caso de uso bastante simples, usando https://github.com/reactjs/react-modal : Eu tenho botões que abrem coisas como menus suspensos e dentro desses botões que criam modais. Clicar nas bolhas modais até o botão superior, fazendo com que ele faça coisas indesejadas. Os modais são encapsulados em um componente coeso e retirando as quebras de parte com portal desse encapsulamento, criando uma abstração que vaza. Uma solução alternativa pode ser inverter um sinalizador para desativar esses botões enquanto o modal está aberto. E, claro, também posso interromper a propagação conforme sugerido acima, embora em alguns casos eu não queira fazer isso.

Não tenho certeza de como o borbulhar e a captura são úteis em geral (embora eu saiba que o React depende do borbulhar sob o capô) - eles certamente têm uma história rica, mas prefiro passar um retorno de chamada ou propagar um evento mais específico (por exemplo, ação redux) do que aumentar ou diminuir, já que essas coisas provavelmente são feitas por meio de um monte de intermediários desnecessários. Existem artigos como https://css-tricks.com/dangers-stopping-event-propagation/ e eu trabalho em aplicativos que dependem de propagação para o corpo, principalmente para fechar coisas ao clicar "fora", mas prefiro coloque uma sobreposição invisível sobre tudo e feche clicando nela. Claro, eu não poderia usar o Portal do React para criar uma sobreposição invisível ...

Há também um pesadelo de manutenção aqui - conforme novos eventos são adicionados ao DOM, quaisquer portais "selados" com a técnica discutida acima "vazarão" esses novos eventos até que os mantenedores possam adicioná-los à (extensa) lista negra.

Há um grande problema de design aqui que precisa ser resolvido. A capacidade de ativar ou desativar o bubbling entre Portal ainda parece a melhor opção de API para mim. Não tenho certeza sobre a dificuldade de implementação, mas ainda estamos recebendo bugs de produção em torno disso no Tableau, mais de um ano depois.

Gaste 2 horas tentando descobrir por que meu formulário modal estava enviando outro formulário.
Finalmente descobri graças a esse problema!

Realmente me esforço para ver quando a propagação de onSubmit pode ser necessária. Provavelmente, sempre será mais como um bug do que um recurso.

Pelo menos vale a pena adicionar algumas informações de aviso para reagir aos
Embora a propagação de eventos pelos portais seja um ótimo recurso, algumas vezes você pode querer evitar a propagação de alguns eventos. Você pode conseguir isso adicionando onSubmit={(e) => {e.stopPropagation()}}

+1 para isso também. Estamos usando draftjs intensamente com texto clicável mostrando modais. E todos os eventos modais, como foco, seleção, alteração, pressionamento de tecla, etc., explodindo rascunhos com erros.

IMO, o comportamento de proxy de evento está fundamentalmente quebrado (e também está me causando bugs), mas reconheço que isso é controverso. Este tópico sugere fortemente que há a necessidade de um portal que bloqueie o contexto, mas não os eventos . A equipe principal concorda? De qualquer forma, qual é o próximo passo aqui?

Eu realmente não consigo perceber porque é que a propagação de eventos do portal é o comportamento pretendido? Isso é totalmente contrário à ideia principal de propagação. Achei que os portais foram criados exatamente para evitar esse tipo de coisa (como aninhamento manual, propagação de eventos, etc.).

Posso confirmar que se você colocar o portal próximo à árvore de elementos, ele propagará eventos:

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 para este pedido de recurso

No DOM, os eventos borbulham na árvore DOM. No React, os eventos borbulham na árvore de componentes.

Confio bastante no comportamento existente, um exemplo disso são os popouts que podem estar aninhados; são todos portais para evitar problemas com overflow: hidden , mas para que o popout se comporte corretamente, preciso detectar cliques externos para o componente popout (que é diferente de detectar cliques fora dos elementos DOM renderizados) . Pode haver exemplos melhores.

Acho que a discussão robusta aqui deixou claro que há boas razões para ter ambos os comportamentos. Como createPortal renderiza um componente React dentro de um nó de contêiner "simples do DOM", não acho que seria viável para os eventos sintéticos do React se propagarem de um Portal até a árvore do DOM simples.

Como os Portals já estão fora do ar há muito tempo, provavelmente é tarde demais para alterar o comportamento padrão para "não se propague além dos limites do portal".

Com base em toda a discussão até agora, minha proposta mais simples é então (ainda): adicionar um sinalizador opcional para createPortal que evita qualquer propagação de evento além do limite do portal.

Algo mais robusto pode ser a capacidade de fornecer uma lista de permissões de eventos que deveriam ter permissão para "cutucar" a fronteira, enquanto interrompiam o resto.

@gaearon Estamos no ponto em que a equipe React pode realmente assumir isso? Este é um dos 10 principais problemas, mas não ouvimos nada de vocês sobre isso há algum tempo.

Quero adicionar meu apoio a isso e discordar dos comentários de @sebmarkbage do ano passado, argumentando que o portal do contexto React e do borbulhamento de eventos DOM faz mais sentido conceitual do que portal apenas contexto.

A capacidade de contextualizar o portal de um local no DOM para outro é útil para implementar todos os tipos de sobreposições, como dicas de ferramentas, menus suspensos, cartões de ajuda e diálogos, onde o conteúdo da sobreposição é descrito por e renderizado no contexto de, o gatilho. Como o contexto é um conceito React, esse mecanismo resolve um problema React. Por outro lado, a capacidade de gerar bolhas de eventos DOM no portal de um local para outro no DOM é um truque sofisticado que permite fingir que a estrutura do DOM é diferente do que foi explicitamente configurado. Isso resolve um problema com o uso de bolhas de eventos DOM para delegação, quando você deseja delegar para uma parte diferente do DOM. Provavelmente, você deve usar callbacks (ou contexto) de qualquer maneira, se tiver React, em vez de depender de eventos DOM borbulhando de dentro para fora da sobreposição. Como outros apontaram, raramente você deseja "alcançar" e lidar com um evento que está acontecendo dentro da sobreposição, intencionalmente ou não.

O bubbling de eventos DOM resolve principalmente um problema de correspondência de eventos DOM com alvos DOM. Cada clique é, na verdade, um clique em um conjunto completo de elementos aninhados. Não é melhor pensado como um mecanismo de delegação de alto nível, IMO, e usar eventos DOM para delegar através dos limites do componente React não é um ótimo encapsulamento, a menos que os componentes sejam pequenos componentes auxiliares privados usados ​​para renderizar bits previsíveis de DOM.

event.target === event.currentTarget me ajuda a resolver esse problema. Mas isso é realmente uma dor de cabeça.

Isso me incomodou hoje ao tentar migrar um componente popover usando unstable_renderSubtreeIntoContainer para usar createPortal . O componente em questão contém elementos arrastáveis ​​e é renderizado como um descendente de outro elemento arrastável. Isso significa que os elementos pai e popover contêm manipuladores de eventos de mouse e toque, que começaram a disparar ao interagir com o popover do portal.

Uma vez que unstable_renderSubtreeIntoContainer está sendo descontinuado (?), Uma solução alternativa é necessária - nenhuma das soluções alternativas apresentadas acima parece ser uma solução viável de longo prazo.

Ei! Obrigado por todas essas sugestões galera!
Isso me ajudou a consertar um dos meus problemas.
Gostaria de ler um artigo excelente e informativo sobre a importância e as habilidades da equipe React ? Acho que será útil para todos que estão interessados ​​em desenvolvimento. Boa sorte!

IMO, é mais comum você querer um portal que lhe dê acesso ao contexto, mas não a eventos borbulhantes. Na época em que usávamos o Angular 1.x, escrevemos nosso próprio serviço pop-up que pegava $scope e uma string de modelo e compilar / renderizar esse modelo e anexá-lo ao corpo. Implementamos todos os pop-ups / modais / suspensos do nosso aplicativo com esse serviço, e nenhuma vez perdemos a falta de borbulhamento de eventos.

A solução alternativa stopPropagation() parece evitar que ouvintes de eventos nativos em window sejam acionados (em nosso caso adicionado por react-dnd-html5-backend ).

Aqui está uma reprodução mínima do problema: https://codepen.io/mogel/pen/xxKRPbQ

Se não houver um plano para fornecer uma maneira de evitar o borbulhamento sintético nos portais, talvez alguém tenha uma solução alternativa que não interrompa o borbulhamento de evento nativo?

A solução alternativa stopPropagation () parece impedir que ouvintes de eventos nativos na janela sejam acionados

Correto. :(

Se não houver um plano de fornecer uma maneira de evitar o borbulhamento sintético pelos portais

Apesar do silêncio da equipe principal, eu e muitos outros neste tópico, _realmente espero_ que existam tais planos.

talvez alguém tenha uma solução alternativa que não interrompa o borbulhamento do evento nativo?

A solução alternativa da minha equipe foi banir totalmente os portais devido a esse problema evidente. Apresentamos painéis com um gancho em um contêiner que vive dentro de outros contextos do aplicativo, para que você obtenha contextos de nível raiz gratuitamente; quaisquer outros que passarmos manualmente. Não é ótimo, mas melhor do que manipuladores de eventos whack-a-mole inúteis.

Já se passaram 17 meses desde a última resposta de alguém da equipe principal. Talvez um ping pudesse chamar a atenção para esse problema :) @sebmarkbage ou @gaearon

A solução alternativa da minha equipe foi banir totalmente os portais devido a esse problema evidente. Apresentamos painéis com um gancho em um contêiner que vive dentro de outros contextos do aplicativo, para que você obtenha contextos de nível raiz gratuitamente; quaisquer outros que passarmos manualmente. Não é ótimo, mas melhor do que manipuladores de eventos whack-a-mole inúteis.

Não consigo pensar em nenhuma abordagem genérica para passar o contexto para o "portal falso" por meio de adereços sem voltar a depender de adereços em cascata :(

Incontáveis ​​foram os bugs que peguei em https://github.com/reakit/reakit que estavam relacionados a esse problema. Eu uso muito o React Portal e não consigo pensar em um único caso em que eu quisesse um evento borbulhando de portais para seus componentes principais.

Minha solução alternativa tem sido verificar dentro de meus manipuladores de eventos pai:

event.currentTarget.contains(event.target);

Ou usando eventos nativos em seu lugar:

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

Eu uso essas abordagens internamente na biblioteca. Mas nenhum deles é ideal. E, como esta é uma biblioteca de componentes de código aberto, não posso controlar como as pessoas passam os manipuladores de eventos para os componentes.

Uma opção para desativar o borbulhamento de eventos resolveria todos esses problemas.

Eu criei uma solução alternativa que bloqueia o borbulhamento do React enquanto também reativa um clone do evento em window . Parece funcionar no Chrome, Firefox e Safari no OSX, mas o IE11 foi deixado de fora por não permitir que event.target seja configurado manualmente. Até agora, ele só se preocupa com eventos de Mouse, Ponteiro, Teclado e Roda. Não tenho certeza se eventos de arrastar seriam clonados.

Infelizmente, ele não pode ser usado em nossa base de código, pois exigimos suporte ao IE11, mas talvez outra pessoa possa adaptá-lo para seu próprio uso.

O que torna isso especialmente incompreensível é que o comportamento 'padrão' em bolhas _ desce_ a árvore de componentes novamente. Pegue a seguinte árvore:

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

Eu tenho puxado meu cabelo por horas, porque com essa combinação exata de componentes o onSubmit do meu formulário nunca é chamado - independentemente se eu clicar no botão enviar ou pressionar Enter em um campo de entrada dentro do formulário.

Finalmente, descobri que é porque o componente React Router Link tem uma implementação onClick que faz e.preventDefault() para evitar que o navegador recarregue. No entanto, isso tem o infeliz efeito colateral de também bloquear o comportamento padrão do clique no botão enviar, que por acaso é o envio do formulário. Então, o que aprendi hoje é que o onSubmit é, na verdade, chamado pelo navegador, como uma ação padrão para pressionar o botão enviar. Mesmo quando você pressiona Enter, ele aciona um clique no botão de envio, acionando assim um envio de formulário.

Mas você vê como a ordem de bolha do evento torna isso realmente muito estranho.

  1. <input> [pressionar a tecla Enter]
  2. <button type="submit"> [clique simulado]
  3. <Menu> [evento se propaga para fora do portal]
  4. <Link> [propagação atinge o pai Link ]
  5. <Link> [chama e.preventDefault() ]
  6. => a resposta padrão do navegador para enviar o clique do botão foi cancelada
  7. => formulário não foi enviado

Isso acontece mesmo que já tenhamos passado o botão e o formulário no DOM, e Link não tem nada a ver com isso e também não pretendíamos bloquear esse comportamento de forma alguma.

A solução para mim (se alguém tiver o mesmo problema) foi a solução comumente usada de envolver o conteúdo <Menu> em um div com onClick={e => e.stopPropagation()} . Mas o que quero dizer é que perdi muito tempo rastreando o problema porque o comportamento é realmente não intuitivo.

A solução para mim (se alguém tiver o mesmo problema) foi a solução comumente usada de envolver o conteúdo <Menu> em uma div com onClick={e => e.stopPropagation()} . Mas o que quero dizer é que perdi muito tempo rastreando o problema porque o comportamento é realmente não intuitivo.

Sim - cada _instância individual do problema_ tem a mesma solução fácil, _uma vez que você experimentou o bug e o identificou corretamente_. É um poço de paredes muito íngreme de falha que a equipe React cavou aqui, e é frustrante não ouvir nenhum reconhecimento disso deles.

Passei vários dias tentando depurar outro problema com mouseenter borbulhando de portais inesperadamente. Mesmo com onMouseEnter={e => e.stopPropagation()} no div do portal, os eventos ainda estão borbulhando para o botão proprietário, como em https://github.com/facebook/react/issues/11387#issuecomment -340009465 (o primeiro comentar sobre este assunto). mouseenter / mouseleave não devem borbulhar ...

Talvez ainda mais estranho, quando vejo uma bolha de evento sintético mouseenter saindo de um portal para um botão, e.nativeEvent.type é mouseout . React está disparando um evento sintético não borbulhante baseado em um evento nativo borbulhante - e apesar de stopPropagation ter sido chamado no evento sintético.

@gaearon @trueadm esse problema tem causado uma frustração enorme e consistente há mais de dois anos. Este tópico é um dos principais problemas ativos no React. Por favor , alguém da equipe poderia contribuir aqui?

No meu caso, abrir o componente Janela clicando em um botão fez a janela desaparecer, pois o clique na janela causou um clique no botão que causou a mudança de estado

Eu sou novo no React, eu uso principalmente um JS jQuery e vanillia, mas este é um bug alucinante. Pode haver cerca de 1% dos casos em que esse comportamento seria esperado ...

Eu gosto das duas soluções de @diegohaz , mas ainda acho que createPortal deveria ter uma opção para parar o borbulhamento de eventos.

Meu caso de uso específico foi com os manipuladores onMouseLeave e onMouseEnter uma dica de ferramenta acionados pelo descendente do portal de seu filho - o que não era desejado. Os eventos nativos consertaram isso ao ignorar os descendentes do portal, uma vez que eles não são descendentes do dom.

1 para a opção de parar de borbulhar nos portais. Foi sugerido apenas colocar o portal como irmão (em vez de filho) do componente de origem do ouvinte de evento, mas acho que isso não funciona em muitos casos de uso (incluindo o meu).

Finalmente parece que ReactDOM.unstable_renderSubtreeIntoContainer será removido , o que significa que em breve não haverá nenhuma solução alternativa razoável para esse problema ...

^ ajude-nos @trueadm -nobi, você é nossa única esperança

Parece que o ping para eles no GitHub não funciona 😞
Talvez alguém com uma conta ativa no Twitter possa twittar sobre isso, marcando um dos contribuidores?

Adicionando meu +1 para este problema. No Notion, atualmente usamos uma implementação de portal personalizada anterior a React.createPortal e encaminhamos manualmente nossos provedores de contexto para a nova árvore. Tentei adotar React.createPortal mas fui bloqueado pelo comportamento inesperado de borbulhamento:

A sugestão de @sebmarkbage de mover <Portal> fora do componente <MenuItem> para se tornar um irmão só resolve o problema para um único nível de aninhamento. O problema permanece se você tiver vários itens de menu aninhados (por exemplo) que geram submenus.

Este problema foi marcado automaticamente como obsoleto. Se esse problema ainda estiver afetando você, deixe um comentário (por exemplo, "bump") e o manteremos aberto. Lamentamos não ter sido possível priorizar ainda. Se você tiver novas informações adicionais, inclua-as em seu comentário!

Ressalto.

Dan deixou um comentário sobre um problema relacionado:

@mogelbrod Atualmente não tenho nada a acrescentar a isso, mas algo como isto ( # 11387 (comentário) ) parece razoável para mim se você estiver migrando um componente existente.

Acompanhamento de Dan na mesma questão :

Obrigado pelo contexto sobre a solução alternativa. Como você já tem esse conhecimento de domínio, a melhor próxima etapa é provavelmente escrever um RFC para o comportamento que deseja e as alternativas consideradas: https://github.com/reactjs/rfcs. Lembre-se de que uma RFC dizendo "vamos apenas mudar isso" provavelmente não será útil. Escrever um bom RFC requer a compreensão de porque temos o comportamento atual, _e_ um plano para mudá-lo de uma forma que se adapte aos seus casos de uso sem regredir em outros.

Independentemente disso , unstable_renderSubtreeIntoContainer não é suportado, então vamos desvendar essas duas discussões. Não adicionaremos propagação de Contexto a ele porque toda a API está congelada e não será atualizada.

Definitivamente, devemos publicar um RFC do React para sugerir a adição do sinalizador discutido, ou talvez outra solução. Alguém se sente particularmente interessado em esboçar um (talvez @justjake , @craigkovatch ou @jquense)? Do contrário, verei o que posso fazer!

Embora eu seja interessante em desenvolver esta API, não tenho interesse em esboçar um RFC. Principalmente bc é um monte de trabalho e quase não há chance de ser aceito, eu não acho que a equipe principal realmente considere RFCs que ainda não estão em seu roadmap.

@jquense Não acho que isso seja preciso. Sim, é improvável que fundamos um RFC que não se alinhe com a visão, pois adicionar uma nova API é sempre transversal e influencia todos os outros recursos planejados. E é justo não comentarmos com frequência sobre os que não funcionam. No entanto, nós os lemos, especialmente quando abordamos um tópico no qual o ecossistema tem mais experiência. Como exemplos, 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 foram todos influentes em nosso pensamento, embora não tenhamos comentado explicitamente sobre eles.

Em outras palavras, abordamos as RFCs em parte como um mecanismo de pesquisa da comunidade. Este comentário de @mogelbrod (https://github.com/facebook/react/issues/16721#issuecomment-674748100) sobre por que a solução alternativa é irritante é exatamente o tipo de coisa que gostaríamos de ver em um RFC. Uma consideração das soluções existentes e suas desvantagens pode ser mais valiosa do que uma proposta de sugestão de API concreta.

@gaearon Meu comentário não é para sugerir que a equipe não

Fico muito feliz em saber que vocês vão olhar para os outros RFCs e que eles contribuem para o design de recursos, mas "Fomos influenciados por esses RFCs externos, embora nunca tenhamos comentado sobre eles", acho que ilustra meu ponto, não desafie-o.

Em outras palavras, abordamos as RFCs em parte como um mecanismo de pesquisa da comunidade.

Isso é muito razoável, mas não é o que o repo RFC diz que _sua_ abordagem é, e não como as outras pessoas geralmente pensam dos RFCs. O processo RFC é geralmente um elo e ponto de comunicação entre a equipe e a comunidade, bem como um campo de jogo equilibrado em termos de análise de recursos e processo.

Pontos maiores sobre governança comunitária à parte. Perguntar às pessoas, gastar tempo redigindo propostas detalhadas e, em seguida, defendê-las para outros participantes externos enquanto é recebido pelo silêncio da equipe de reação é desanimador e fortalece ativamente a impressão de que FB só se preocupa com suas próprias necessidades de OSS. O que fede, porque eu sei que você não sentirá ou projetará assim.

Se o processo RFC deveria ser: "aqui é onde você pode delinear suas preocupações e casos de uso e nós os leremos, quando / se chegarmos a um ponto de sermos capazes de implementar esse recurso". Honestamente, essa é uma boa abordagem. Acho que a comunidade se beneficiaria com isso explicitamente explicitado, caso contrário, o pessoal irá (e fará) assumir o mesmo nível de envolvimento e participação que outros processos RFC costumam ter e, então, ser ativamente desencorajado quando isso não funcionar. Certamente tenho essa impressão, mesmo com um pouco mais de percepção do que outros contribuidores.

Claro, acho que concordo com tudo isso. Não quero transformar isso em um meta-tópico, mas apenas dizendo que, como as pessoas continuam pingando sobre esse tópico, a coisa mais acionável a fazer para avançar é escrever uma proposta de como ele deve funcionar que leve em conta os dois lados em conta e compila uma compreensão profunda do espaço do problema . Eu entendo perfeitamente se isso não é algo que as pessoas gostariam de aprofundar (em parte com base em como respondemos aos RFCs), e é por isso que não sugeri isso antes - mas, como continuo recebendo pings pessoais, gostaria de sugerir que como opção para quem está motivado.

justo, este não é o lugar certo para obter meta sobre RFCs :)

@gaearon, esta é a 6ª edição mais votada atualmente aberta no React, e a 4ª mais comentada. Ele está aberto desde que o React 16 foi lançado, e está a apenas 2 meses de completar 3 anos agora. No entanto, houve muito pouco envolvimento da equipe principal do React. Parece muito desdenhoso dizer "depende da comunidade propor uma solução" depois de tanto tempo e dor ter passado e ocorrer. Por favor, reconheça que, embora ele tenha alguns aplicativos muito úteis, esse comportamento sendo o padrão foi um erro de design. Não deve caber à comunidade a RFC consertá-lo.

Lamento comentar sobre este assunto e retiro minha sugestão sobre a comunidade RFC. Você está certo, provavelmente é uma má ideia. Devo acrescentar que esta questão se tornou muito emocionalmente carregada e, como ser humano, pessoalmente acho difícil me envolver com ela - embora eu entenda que é importante e muitas pessoas tenham opiniões fortes sobre isso.

Deixe-me responder brevemente sobre o estado deste tópico.

Em primeiro lugar, gostaria de pedir desculpas às pessoas que comentaram e ficaram frustradas por não darmos continuidade aos acompanhamentos neste tópico. Se eu estivesse lendo este problema de fora, minha impressão provavelmente teria sido que a equipe React cometeu um erro, não está disposta a admiti-lo e está disposta a se sentar em uma solução simples ("basta adicionar um booleano, quão difícil pode seja! ") por mais de dois anos porque eles não se importam com a comunidade. Posso entender perfeitamente como você pode chegar a essa conclusão.

Eu sei que este problema é altamente votado. Isso foi levantado várias vezes neste tópico, talvez da perspectiva de que, se a equipe do React soubesse que isso é um grande problema, teríamos resolvido isso antes. Sabemos que isso é um ponto problemático - as pessoas regularmente nos enviam mensagens em particular sobre isso ou usam isso como um exemplo de como a equipe React não se preocupa com a comunidade. Embora eu reconheça plenamente que o silêncio tem sido frustrante, a pressão crescente para "apenas fazer algo" tornou mais difícil lidar com essa questão de forma produtiva.

Esse problema tem soluções alternativas - o que o torna diferente de uma vulnerabilidade de segurança ou uma falha que precisa ser tratada com urgência. Sabemos que wokarounds funcionam (mas não são ideais e podem ser irritantes) porque usamos alguns deles nós mesmos, especialmente em torno de código que foi escrito antes do React 16. Acho que podemos concordar que, embora este problema tenha sido indubitavelmente frustrante para um grande número de pessoas, ele ainda está em uma classe de problemas diferente de uma falha ou questão de segurança que deve ser respondida dentro de um prazo concreto.

Além disso, discordo do enquadramento de que existe uma solução simples que podemos implementar amanhã. Mesmo se considerarmos o comportamento inicial um erro (com o qual não tenho certeza se concordo), isso significa que a barreira para que o próximo comportamento trate de toda a variedade de casos de uso é ainda maior . Se consertarmos alguns casos, mas resolvermos outros, não fizemos nenhum progresso e criamos uma tonelada de rotatividade. Lembre-se de que não ouviremos sobre os casos em que o comportamento atual funciona bem neste problema. Só ouviremos sobre isso depois de quebrá-lo.

Para dar um exemplo, o comportamento atual é realmente muito útil para o caso de uso de gerenciamento de foco declarativo que temos pesquisado há algum tempo. É útil tratar o foco / desfoque como acontecendo "dentro" de um modal em relação à árvore de partes, apesar de ser um Portal. Se enviássemos a proposta "simples" createPortal(tree, boolean) sugerida neste thread, esse caso de uso não funcionaria porque o próprio portal não pode "saber" sobre qual comportamento queremos. Qualquer exploração de uma possível solução precisa considerar dezenas de casos de uso e alguns deles ainda não são totalmente compreendidos. Isso é necessário em algum momento, com certeza, mas também é um grande compromisso de tempo para fazer da maneira certa e até agora não fomos capazes de nos concentrar nisso.

Os eventos em particular são uma área espinhosa, por exemplo, acabamos de fazer um monte de mudanças que tratam de anos de problemas, e este tem sido um grande foco este ano. Mas só podemos fazer tantas coisas ao mesmo tempo.

Geralmente, nós, como equipe, tentamos nos concentrar em poucos problemas profundamente, em vez de em muitos problemas superficialmente. Infelizmente, isso significa que algumas falhas e lacunas conceituais podem não ser preenchidas por anos porque estamos no meio de consertar outras lacunas importantes ou não temos um projeto alternativo elaborado que teria resolvido o problema para sempre. Eu sei que isso é frustrante de ouvir, e é uma parte da razão pela qual me mantive longe deste tópico. Alguns outros tópicos semelhantes se transformaram em explicações mais profundas dos problemas e possíveis soluções, que são úteis, mas este se transformou principalmente em uma enxurrada de "+1" e sugestões para uma solução "simples", que é o motivo de ter sido difícil para se envolver com isso de forma significativa.

Sei que essa não é a resposta que as pessoas queriam ouvir, mas espero que seja melhor do que nenhuma resposta.

Outra coisa que vale a pena destacar é que alguns dos pontos fracos descritos neste tópico podem ter sido resolvidos por outros meios. Por exemplo:

Especificamente: chamar stopPropagation no evento de focus sintético para evitar que borbulhe para fora do portal faz com que stopPropagation também seja chamado no evento de focus nativo no manipulador capturado do React em #document, o que significa que não foi para outro manipulador capturado em

O React não usa mais a fase de captura para emular o bubbling e também não escuta mais eventos no documento. Portanto, sem descartar a frustração, com certeza será necessário reavaliar tudo o que foi postado até agora à luz das outras mudanças.

Os eventos nativos ainda borbulham e, como nosso código React está hospedado em um aplicativo principalmente jQuery, o manipulador global jQuery keyDown em

ainda consegue o evento.

Da mesma forma, o React 17 anexará eventos às raízes e aos contêineres do portal (e realmente interromperá a propagação nativa nesse ponto), portanto, também esperaria ser resolvido.

Com relação aos pontos sobre renderSubtreeIntoContainer sendo removidos. Literalmente, sua única diferença de ReactDOM.render é que ele propaga o contexto legado. Uma vez que qualquer lançamento que não incluísse renderSubtreeIntoContainer também não incluiria o contexto legado, ReactDOM.render permaneceria uma alternativa 100% idêntica. Isso, é claro, não resolve o problema mais amplo, mas acho que a preocupação com renderSubtree especificamente está um pouco mal colocada.

@gaearon

Com relação aos pontos sobre renderSubtreeIntoContainer sendo removidos. Literalmente, sua única diferença de ReactDOM.render é que ele propaga o contexto legado. Uma vez que qualquer lançamento que não incluísse renderSubtreeIntoContainer também não incluiria o contexto legado, ReactDOM.render permaneceria uma alternativa 100% idêntica. Isso, é claro, não resolve o problema mais amplo, mas acho que a preocupação com renderSubtree especificamente está um pouco mal colocada.

Agora que você mencionou, gostaria de saber se o código abaixo seria uma implementação válida e segura para um Portal React sem bolhas de eventos:

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 com alguns testes: https://codesandbox.io/s/react-portal-with-reactdom-render-m22dj?file=/src/App.js

Ainda há um problema em não passar o Contexto Moderno, mas este não é um problema novo ( renderSubtree também é afetado por ele). A solução alternativa é cercar sua árvore com vários provedores de contexto. No geral, não é ideal aninhar árvores, então eu não recomendaria mudar para esse padrão em outra coisa senão em cenários de código existentes legados.

Mais uma vez, muito obrigado pelo artigo @gaearon!

Parece que agregar a lista de casos quebrados + soluções alternativas (atualizada para React v17) seria a coisa mais produtiva para alguém fora da equipe principal (corrija-me se eu estiver errado!).

Estou sobrecarregado nas próximas semanas, mas pretendo fazer o mais rápido possível. Se alguém mais puder fazer isso antes, ou interferir com trechos (como

Agregar uma lista de casos seria definitivamente útil, embora eu diga que é necessário incluir não apenas os casos interrompidos, mas também os casos em que o comportamento atual faz sentido.

Se houver um espaço público para adicionar, ficaria feliz em adicionar casos de uso de nossos aplicativos e como autor de uma biblioteca de IU. Em geral, concordo com Dan que, embora às vezes seja irritante, é fácil de contornar. Para casos em que você deseja o borbulhar do React, é muito difícil cobrir o caso sem a ajuda do React.

Agregar uma lista de casos seria definitivamente útil, embora eu diga que é necessário incluir não apenas os casos interrompidos, mas também os casos em que o comportamento atual faz sentido.

Eu ficaria feliz em incluí-los se alguém pudesse me indicar algum código-fonte aberto / código extraído que dependa disso! Como você mencionou anteriormente, é um pouco difícil de encontrar, pois apenas pessoas com problemas com o comportamento atual estão envolvidas neste problema 😅

Se houver um espaço público para adicionar, ficaria feliz em adicionar casos de uso de nossos aplicativos e como autor de uma biblioteca de IU. Em geral, concordo com Dan que, embora às vezes seja irritante, é fácil de contornar. Para casos em que você deseja o borbulhar do React, é muito difícil cobrir o caso sem a ajuda do React.

Qualquer espaço específico que você tem em mente ou compartilhar um codesandbox (ou jsfiddle, etc.) por caso funcionaria como um iniciador? Posso tentar compilá-los todos depois de reunir alguns casos.

Comecei um tópico aqui: https://github.com/facebook/react/issues/19637. Vamos mantê-lo focado em exemplos práticos, enquanto este permanece para uma discussão geral.

Esta página foi útil?
0 / 5 - 0 avaliações