React: createPortal: опция поддержки для остановки распространения событий в дереве React

Созданный на 27 окт. 2017  ·  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 , я прерву срабатывание события, когда мышь окажется над кнопкой. Если я сделаю это в всплывающем окне и использую какой-то общий компонент всплывающего окна для этого приложения, я также могу нарушить логику в других частях приложения.

Я действительно не понимаю, зачем нужно пролистывать дерево 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, и это можно рассматривать как ошибку .

Ребята, спасибо за обсуждение, однако я не думаю, что спорить о том, является ли что-то ошибкой или нет, полезно. Вместо этого я был бы более продуктивным, чтобы обсудить варианты использования и примеры, которые не соответствуют текущему поведению, чтобы мы могли лучше понять, является ли текущий путь лучшим путем на будущее.

В общем, мы хотим, чтобы API обрабатывал разнообразный набор сценариев использования, но, надеюсь, не слишком ограничивал другие. Я не могу говорить от имени основной команды, но полагаю, что сделать ее настраиваемой - маловероятное решение. Обычно React предпочитает согласованный API настраиваемым.

Я также понимаю, что такое поведение не соответствует принципу работы DOM, но я не думаю, что это само по себе веская причина говорить, что так быть не должно. Во многом поведение react-dom отличается от того, как работает DOM, возможно, события уже отличаются от нативной версии. Например, onChange совершенно не похож на собственное событие изменения, и все реагирующие события всплывают независимо от типа, в отличие от DOM.

Вместо этого я был бы более продуктивным, чтобы обсудить варианты использования и примеры, которые не соответствуют текущему поведению.

Вот два примера, которые сломались для нас при переходе на React 16.

Во-первых, у нас есть перетаскиваемый диалог, который запускается кнопкой. Я попытался добавить «фильтрующий» элемент на нашем портале, который вызывал StopPropagation для любых событий мыши * ключа *. Однако мы полагаемся на возможность привязать событие mousemove к документу, чтобы реализовать функцию перетаскивания - это обычное дело, потому что, если пользователь перемещает мышь со значительной скоростью, курсор покидает границы диалогового окна, и вам нужно чтобы иметь возможность фиксировать движение мыши на более высоком уровне. Фильтрация этих событий нарушает эту функциональность. Но с порталами события мыши и клавиш перетекают изнутри диалогового окна к кнопке, которая его запустила, заставляя отображать различные визуальные эффекты и даже закрывать диалоговое окно. Я не думаю, что реалистично ожидать, что каждый компонент, который будет запущен через портал, будет связывать 10-20 обработчиков событий, чтобы остановить распространение этого события.

Во-вторых, у нас есть всплывающее контекстное меню, которое может быть запущено либо основным, либо второстепенным щелчком мыши. У одного из внутренних потребителей нашей библиотеки есть обработчики мыши, прикрепленные к элементу, который запускает это меню, и, конечно же, в меню также есть обработчики щелчков для обработки выбора элемента. Теперь меню снова появляется при каждом щелчке, поскольку события mousedown / mousedown возвращаются к кнопке, которая запускает меню.

Я не могу говорить от имени основной команды, но полагаю, что сделать ее настраиваемой - маловероятное решение. Обычно React предпочитает согласованный API настраиваемым.

Умоляю вас (и команду) пересмотреть эту позицию в данном конкретном случае. Я думаю, что всплытие событий будет интересно для определенных случаев использования (хотя я не могу придумать ничего навскидку). Но я думаю, что это будет парализовать других и внесет существенную несогласованность в API. Хотя unstable_rendersubtreeintocontainer никогда не пользовался суперподдержкой, это было то, что все использовали для рендеринга за пределами непосредственного дерева, и это не сработало. Официально он был объявлен устаревшим в пользу порталов, но порталы нарушают функциональность таким критическим образом, и, похоже, нет простого обходного пути. Я думаю, что это справедливо можно охарактеризовать как весьма противоречивое.

Я также понимаю, что такое поведение не соответствует принципу работы DOM, но я не думаю, что это само по себе веская причина говорить, что так быть не должно.

Я понимаю, откуда вы пришли, но я думаю, что в этом случае (а) это фундаментальное поведение, которое (б) в настоящее время не имеет обходного пути, поэтому я думаю, что «DOM не работает таким образом» - сильный аргумент, если не совсем убедительный.

И для ясности: моя просьба считать это ошибкой в ​​основном для того, чтобы исправить это раньше, чем позже.

Моя ментальная модель портала состоит в том, что он ведет себя так, как если бы он находится в том же месте в дереве, но избегает таких проблем, как «переполнение: скрыто», и избегает прокрутки для целей рисования / макета.

Есть много подобных «всплывающих» решений, которые работают без портала. Например, кнопка, которая расширяет поле рядом с ней.

Возьмем, к примеру, диалог «Выберите свою реакцию» здесь, на GitHub. Это реализовано как div рядом с кнопкой. Теперь это прекрасно работает. Однако, если он хочет иметь другой z-индекс или быть извлеченным из области overflow: scroll , содержащей эти комментарии, тогда ему нужно будет изменить положение DOM. Это изменение небезопасно делать, если не сохранены другие вещи, такие как всплытие событий.

Оба стиля «всплывающих окон» или «всплывающих окон» допустимы. Итак, как бы вы решили ту же проблему, когда компонент встроен в макет, а не плавает вне его?

Обходной путь, который сработал для меня, - это вызов stopPropagation непосредственно при рендеринге портала:

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

Это отлично работает для меня, поскольку у меня есть единственный компонент абстракции, который использует порталы, иначе вам нужно будет исправить все ваши вызовы createPortal .

@methyl это предполагает, что вы знаете каждое событие, которое вам нужно заблокировать от всплытия на дереве. И в случае, о котором я упоминал, с перетаскиваемыми диалоговыми окнами, нам нужно mousemove чтобы всплыть в документ, но не всплыть в дереве рендеринга.

Оба стиля «всплывающих окон» или «всплывающих окон» допустимы. Итак, как бы вы решили ту же проблему, когда компонент встроен в макет, а не плавает вне его?

@sebmarkbage Я не уверен, что этот вопрос имеет смысл. Если бы у меня возникла эта проблема при встраивании компонента, я бы не стал его встраивать.

Я думаю, что одна из проблем здесь заключается в том, что некоторые варианты использования renderSubtreeIntoContainer переносятся на createPortal когда два метода делают концептуально разные вещи. Думаю, концепция Portal была перегружена.

Я согласен с тем, что в случае модального диалога вы почти никогда не хотите, чтобы модальное окно действовало как дочерний элемент кнопки, которая его открыла. Компонент триггера отображает его только потому, что контролирует состояние open . Я думаю, что было бы ошибкой сказать, что реализация портала неправильна, вместо того, чтобы сказать, что createPortal в кнопке не является подходящим инструментом для этого. В этом случае модальное окно не является дочерним по отношению к триггеру, и его не следует отображать так, как если бы он был. Одно из возможных решений - продолжать использовать renderSubtreeIntoContainer , другой вариант для пользователя - иметь ModalProvider рядом с корнем приложения, который обрабатывает модальные окна и передает (через контекст) метод для рендеринга произвольный модальный элемент нужен корню

renderSubtreeIntoContainer нельзя вызвать изнутри render или методов жизненного цикла в React 16, что в значительной степени исключает его использование в тех случаях, которые я обсуждал (фактически, все наши компоненты, которые были выполнение этого полностью сломалось при переходе на 16). Официальная рекомендация - порталы: https://reactjs.org/blog/2017/09/26/react-v16.0.html#breaking -changes

Я согласен с тем, что концепция порталов могла оказаться перегруженной. Не уверен, что мне нравится решение глобального компонента и контекст для него. Кажется, что это можно легко решить, установив флаг на 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, темы и т. Д.)

Я скажу, что этот вариант использования _ может_ быть в основном решен с помощью ModalProvider ближе к корню приложения, который отображает через createPortal достаточно высокий уровень, чтобы распространение событий ни на что не влияло, но это начинает казаться обходным путем в отличие от хорошо продуманная архитектура. Это также делает предоставленные библиотекой модальные окна более раздражающими для пользователей, поскольку они больше не являются самодостаточными.

Я бы добавил, что с точки зрения API, я не думаю, что createPortal должен делать и то, и другое, модальный случай действительно хочет просто использовать ReactDOM.render (старый skool), потому что он довольно близок к отдельному дереву _except_ это распространение контекста часто требуется

Нам просто нужно было исправить чрезвычайно сложную для диагностики ошибку в коде управления фокусом нашего внешнего приложения в результате использования обходного пути, опубликованного @ kib357 .

В частности: вызов stopPropagation в событии синтетического фокуса, чтобы предотвратить его выход из портала, вызывает также вызов stopPropagation в собственном событии фокуса в захваченном обработчике React на #document, что означает, что он не попал в другой захваченный обработчик на <body> . Мы исправили это, переместив наш обработчик в #document, но мы специально избегали этого в прошлом, чтобы не наступать React на пятки.

Мне кажется, что новое всплывающее поведение в Portals - дело меньшинства. Будь то мнение или правда, не могли бы мы обсудить этот вопрос? Может, @gaearon? Ему четыре месяца, и он вызывает настоящую боль. Я думаю, что это справедливо можно назвать ошибкой, учитывая, что это критическое изменение API в React 16 без полностью безопасного обходного пути.

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

Если всплывающее окно является порталом, оно работает, и при наведении курсора мыши на портал не отображается никаких событий. Но что более важно, он также работает, если это НЕ портал, и он должен быть встроенным всплывающим элементом. StopPropagation не требуется.

Так что же такого в этом шаблоне, что не работает для вашего варианта использования?

@sebmarkbage мы используем порталы совершенно по-другому, рендеринг в контейнер, установленный как последний дочерний элемент <body> который затем позиционируется, иногда с z-индексом. Документация 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 модальный регистр обычно требует контекста с момента его рендеринга. Это немного уникально для случая, я думаю, компонент является логическим дочерним элементом того, что его визуализировало, но _не_ структурным (из-за отсутствия лучшего слова), например, вам обычно нужны такие вещи, как контекст формы (relay, formik, redux form , что угодно), но не события DOM, которые нужно пройти. Один также заканчивает рендерингом таких модальных окон довольно глубоко в деревьях, рядом с их триггерами, поэтому они остаются компонентными и пригодными для повторного использования, в большей степени, чем потому, что они структурно принадлежат им.

Я думаю, что этот случай в целом отличается от раскрывающегося / раскрывающегося списка createPortal. Tbc Я считаю, что пузырьковое поведение порталов - это хорошо, но не для модальных окон. Я также думаю, что с этим можно справиться с помощью Context и какого-то ModalProvider достаточно хорошо, но это немного раздражает, особенно для библиотек.

до тех пор, пока вы поднимаете портал до родного брата зависшего компонента, а не его дочернего элемента, ваш сценарий работает.

Не уверен, что понимаю. По-прежнему существует проблема, например, прохождения событий keyDown через неожиданное дерево DOM.

@jquense Обратите внимание, что в моем примере слот все еще находится в компоненте Bar, поэтому он мог бы получить свой контекст из формы в виде чего-то вроде <Form><Bar /></Form> .

Даже если портал визуализирован в тело документа.

Так что это похоже на два косвенных обращения (портал): глубокий -> брат Бар -> тело документа.

Таким образом, контекст портала по-прежнему является контекстом формы, как и цепочка всплывающих событий, но ни то, ни другое не находится в контексте зависшего объекта.

Да, прости, что пропустил 😳 Если я все же читаю правильно, у вас все равно будет пузырение на уровне <Slot> вверх? Это определенно лучше, хотя я думаю, что в случае модального диалогового окна, вероятно, не нужно _any_ пузыриться. Как и в терминах программы чтения с экрана, вы хотите, чтобы все, что находится за пределами модального окна, было инвертировано, пока оно работает. Я не знаю, я думаю, что в этом случае всплытие - это ошибка, никто не ожидал, что щелчок внутри диалогового окна всплывет где-нибудь.

Может быть, проблема здесь не в порталах, но нет хорошего способа поделиться контекстом между деревьями? Часть контекста ReactDOM.render действительно хороша для модальных окон, и, возможно, в любом случае более "правильный" способ думать об этом ...

Я думаю, что есть некоторое всплытие, потому что оно все еще идет от модального к div, к телу, документу и окну. И концептуально за рамкой до содержащего окно и так далее.

Это не теоретически для чего-то вроде визуализированного контента ART или GL (и в некоторой степени React Native), где может не быть существующего дерева поддержки, из которого можно было бы получить эту семантику. Так что должен быть способ сказать, что именно здесь он пузырится.

В некоторых приложениях есть модальные окна в модальных окнах. Например, в FB есть окно чата, которое может быть над модальным, или модальное окно может быть частью окна чата. Таким образом, даже у модального окна есть некоторый контекст относительно того, к какому месту в дереве он принадлежит. Он никогда не бывает полностью автономным.

Это не значит, что у нас не может быть двух разных семантик для восходящей цепочки событий и контекста. Они оба прямо об этом говорят, и вы можете портить одно без другого и т. Д.

Гарантия того, что они оба следуют по одному и тому же пути, действительно очень эффективна, поскольку это означает, что всплытие событий может быть полностью реализовано для событий пользовательского пространства так же, как в браузере.

Например, сегодня это происходит с различными контекстами Redux. Представьте, что this.context.dispatch("Hover") - всплывающее событие пользовательского пространства. Мы могли бы даже реализовать события React как часть контекста. Кажется разумным думать, что я могу использовать это таким же образом, и прямо сейчас вы можете использовать его всеми способами. Я думаю, что если бы мы сделали форк этих двух контекстов, мы, вероятно, получили бы другой API контекста пользовательского пространства, который следует структуре DOM параллельно с обычным контекстом - если они действительно настолько разные.

Вот почему я немного выступаю против этого, чтобы увидеть, может ли быть достаточно слотов, поскольку а) вам нужно четко указать, какой контекстный всплытие происходит в любом случае. б) он может избежать разветвления мира и наличия двух полных контекстных систем.

В частности: вызов stopPropagation для события синтетического фокуса, чтобы предотвратить его выход из портала, приводит к тому, что stopPropagation также вызывается в собственном событии фокуса в захваченном обработчике React на #document, что означает, что он не попал в другой захваченный обработчик на

. Мы исправили это, переместив наш обработчик в #document, но мы специально избегали этого в прошлом, чтобы не наступать React на пятки.

@craigkovatch , вы использовали событие onFocusCapture в документе? В моем обходном пути захваченные события не должны останавливаться. Не могли бы вы привести более подробный пример того, как это было и что вы сделали для решения своей проблемы?
Кроме того, я думаю, что в моем коде есть проблема с остановкой события blur - его не следует останавливать. Итак, я исследую этот вопрос глубже и постараюсь найти более надежное решение.

@ kib357 Я не предполагаю, что есть проблема в вашем

В рассматриваемом коде используется собственный прослушиватель событий захвата, то есть document.body.addEventListener('focus', handler, true)

@craigkovatch звучит интересно, учитывая тот факт, что вы использовали захваченный обработчик. Однако у меня нет никаких мыслей, почему это происходит.

Итак, ребята, у нас есть два разных сценария использования рендеринга портала:

  1. Чтобы предотвратить проблемы CSS, такие как переполнение: скрыто и т. Д., В простых виджетах, таких как кнопки раскрывающегося списка или одноуровневые меню
  2. Чтобы создать новый уровень UX для более мощных случаев, таких как:
  3. модальные окна
  4. вложенные меню
  5. popovers-with-forms-with-dropdown -... - все случаи, когда слои объединяются

Я думаю, что текущий 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, глобальный обработчик jQuery keyDown на <body> прежнему получает событие.

Я попытался добавить прослушиватель event.stopPropagation к собственному элементу контейнера внутри портала с помощью такой ссылки, но это полностью нейтрализует все синтетические события в портале - я ошибочно предположил, что прослушиватель верхнего уровня 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>

А вот гифка с похожей, но немного другой настройкой, где я использую createPortal для адаптивного сайта, чтобы переместить поле формы на панель инструментов приложения (намного выше по дереву). В этом случае я также действительно не хочу, чтобы события возвращались к содержимому страницы, но я определенно хочу, чтобы контекст формы шел вместе с ним. Моя реализация, кстати, - это какая-то Slot-esque вещь, использующая контекст ...

large gif 640x320

@sebmarkbage unstable_renderSubtreeIntoContainer разрешает прямой доступ к вершине иерархии независимо от положения компонента, либо внутри иерархии, либо как часть отдельной упакованной структуры.

Для сравнения я вижу несколько проблем со слотом:

  • Решение предполагает, что у вас есть доступ к той позиции в иерархии, где можно «всплывать» событиями. Это определенно не относится к компонентам и компонентным каркасам.
  • Предполагает, что «нормально» всплывать событиями на любом другом уровне иерархии.
  • События по-прежнему будут всплывать с позиции слота. (как упомянул @craigkovatch )

У меня также есть вариант использования (вероятно, похожий на уже упомянутые).

У меня есть поверхность, на которой пользователи могут выбирать вещи мышкой с помощью «лассо». Это в основном 100% ширина / высота, она находится в корне моего приложения и использует событие onMouseDown . На этой поверхности также есть кнопки, которые открывают порталы, такие как модальные окна и раскрывающиеся списки. Событие mouseDown внутри портала фактически перехватывается компонентом выбора лассо в корне приложения.

Я вижу много способов исправить проблему:

  • отобразить портал на один шаг выше корневого компонента лассо, но это не очень удобно и, вероятно, придется прибегать к контекстной библиотеке, такой как react-gateway? (или, может быть, упомянутая система слотов).
  • остановить распространение вручную внутри корня портала, но это может привести к нежелательным побочным эффектам, упомянутым выше
  • возможность остановить распространение на порталах React (+1 кстати)
  • отфильтровать события, когда они приходят с портала

На данный момент мое решение - отфильтровать события.

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

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


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

Это явно не будет лучшим решением для всех нас и будет не очень хорошо, если у вас есть вложенные порталы, но для моего текущего варианта использования (который на данный момент является единственным) он работает. Я не хочу добавлять новую контекстную библиотеку или выполнять сложный рефакторинг, чтобы решить эту проблему. Просто хотел поделиться своим решением.

Мне удалось выполнить всплытие событий блокировки, как указано в другом месте в этом потоке.

Но еще одна, казалось бы, более неприятная проблема, с которой я сталкиваюсь, - это onMouseEnter SyntheticEvent, которое не всплывает. Скорее, он переходит от общего родителя компонента from компоненту to как описано здесь . Это означает, что если указатель мыши входит извне окна браузера, каждый обработчик onMouseEnter от верхней части DOM до компонента в createPortal будет запускаться в этом порядке, вызывая запуск всех видов событий, которые никогда не делал с unstable_renderSubtreeIntoContainer . Поскольку onMouseEnter не всплывает, его нельзя заблокировать на уровне портала. (Это не похоже на проблему с unstable_renderSubtreeIntoContainer поскольку событие onMouseEnter не учитывает виртуальную иерархию и не последовательно проходит через содержимое тела, а спускается непосредственно в поддерево.)

Если у кого-то есть идеи, как предотвратить распространение событий onMouseEnter из верхней части иерархии DOM или перенаправления непосредственно в поддерево портала, дайте мне знать.

@JasonGore Я тоже заметил такое поведение.

Например.

У меня есть контекстное меню, которое отображается, когда div запускает onMouseOver, затем я открываю модальное окно с помощью createPortal, щелкнув один из элементов меню. Когда я выношу мышь из окна браузера, событие onMouseLeave распространяется до контекстного меню, закрывая контекстное меню (и, следовательно, модальное) ...

У меня была такая же проблема, когда у меня был элемент списка, который я хотел, чтобы все было интерактивным (как ссылка), но мне нужна была кнопка удаления на ярлыках под именем, которая открывала бы модальное окно для подтверждения.

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 (как для стилей, так и для изоляции событий), и не можем предотвратить всплытие событий из коробки, это облом.

Похоже, это 12-я проблема в бэклоге React. По крайней мере, документы об этом открыты 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 . Скорее всего, это всегда будет скорее ошибка, чем функция.

По крайней мере, стоит добавить некоторую предупреждающую информацию, чтобы реагировать на документы . Что-то вроде этого:
Хотя передача событий через порталы - отличная функция, иногда вам может потребоваться предотвратить распространение некоторых событий. Вы можете добиться этого, добавив onSubmit={(e) => {e.stopPropagation()}}

+1 к этому тоже. Мы часто используем draftjs с интерактивным текстом, отображающим модальные окна. И все события в модальном режиме, такие как фокус, выбор, изменение, нажатие клавиш и т. Д., Просто взрывают 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 визуализирует компонент React внутри узла контейнера «простой DOM», я не думаю, что для синтетических событий React было бы целесообразно распространяться из портала вверх по дереву простой старой DOM.

Поскольку порталы уже давно отсутствуют, вероятно, уже слишком поздно менять поведение по умолчанию на «не распространяться за границу портала».

Основываясь на всем обсуждении до сих пор, мое простейшее предложение (все еще): добавить необязательный флаг в createPortal, который предотвращает распространение любого события за границу портала.

Что-то более надежное - это возможность предоставить белый список событий, которым следует разрешить «проталкивать» границу, останавливая при этом остальные.

@gaearon Настали ли мы, что команда React действительно могла бы взяться за это? Это проблема первой десятки, но мы давно ничего не слышали от вас по этому поводу.

Я хочу добавить к этому свою поддержку и не согласен с прошлогодними комментариями @sebmarkbage, в которых утверждается, что перенос как контекста React, так и всплытия событий DOM имеет более концептуальный смысл, чем перенос просто контекста.

Возможность переносить контекст из одного места в DOM в другое полезно для реализации всевозможных наложений, таких как всплывающие подсказки, раскрывающиеся списки, подсказки и диалоговые окна, где содержимое наложения описывается и отображается в контексте, триггер. Поскольку контекст - это концепция React, этот механизм решает проблему React. С другой стороны, возможность переносить всплытие событий DOM из одного места в DOM в другое - это хитрый трюк, который позволяет вам притвориться, что структура DOM отличается от той, которую вы явно задали. Это решает проблему с использованием восходящей цепочки событий DOM для делегирования, когда вы хотите делегировать другую часть DOM. Вероятно, вам все равно следует использовать обратные вызовы (или контекст), если у вас есть React, а не полагаться на события DOM, всплывающие изнутри наружу наложения. Как отмечали другие, вы редко хотите «проникнуть внутрь» и обработать событие, происходящее внутри оверлея, намеренно или непреднамеренно.

Барботаж событий DOM в первую очередь решает проблему сопоставления событий DOM с целями DOM. Каждый щелчок на самом деле является щелчком по целому набору вложенных элементов. Его не лучше рассматривать как механизм делегирования высокого уровня, IMO, и использование событий DOM для делегирования через границы компонентов React не является хорошей инкапсуляцией, если только компоненты не являются небольшими частными вспомогательными компонентами, используемыми для рендеринга предсказуемых битов DOM.

event.target === event.currentTarget помогает мне решить эту проблему. Но это настоящая головная боль.

Это укусило меня сегодня, когда я пытался перенести компонент popover, используя unstable_renderSubtreeIntoContainer чтобы использовать createPortal . Рассматриваемый компонент содержит перетаскиваемые элементы и отображается как потомок другого перетаскиваемого элемента. Это означает, что и родительский элемент, и элемент popover содержат обработчики событий мыши и касания, которые запускаются при взаимодействии с popover на портале.

Поскольку unstable_renderSubtreeIntoContainer устарел (?), Необходимо альтернативное решение - ни один из обходных путей, представленных выше, не является жизнеспособным долгосрочным решением.

Привет! Спасибо за все эти предложения, ребята!
Это помогло мне исправить одну из моих проблем.
Хотели бы вы прочитать отличную и информативную статью о важности и возможностях команды React ? Думаю, это будет полезно всем, кто интересуется развитием. Удачи!

ИМО, чаще вы хотите, чтобы портал давал вам доступ к контексту, но не всплывал событиями. Когда мы использовали Angular 1.x, мы написали нашу собственную службу всплывающих окон, которая брала $scope и строку шаблона, компилировала / рендерила этот шаблон и добавляла его в тело. Мы реализовали все всплывающие / модальные / выпадающие меню нашего приложения с помощью этой службы, и мы ни разу не упустили отсутствие всплытия событий.

Обходной путь stopPropagation() видимому, предотвращает запуск собственных прослушивателей событий на window (в нашем случае добавлен react-dnd-html5-backend ).

Вот минимальное изображение проблемы: https://codepen.io/mogel/pen/xxKRPbQ

Если нет плана предоставить способ избежать синтетического пузыря через порталы, возможно, у кого-то есть обходной путь, который не нарушает пузырение собственных событий?

Обходной путь stopPropagation () предотвращает запуск собственных прослушивателей событий в окне

Верный. :(

Если нет плана предоставить способ избежать синтетического пузыря через порталы

Несмотря на молчание основной группы, я и многие другие участники этой ветки _ очень надеюсь_, что такие планы есть.

возможно, у кого-то есть обходной путь, который не прерывает всплытие нативных событий?

Моя команда решила полностью запретить порталы из-за этой вопиющей проблемы. Мы представляем панели с перехватчиком в контейнере, который находится в других контекстах приложения, поэтому вы получаете контексты корневого уровня бесплатно; любые другие, которые мы передаем вручную. Не очень хорошо, но лучше, чем бессмысленные обработчики событий типа «ударить крота».

Прошло 17 месяцев с момента последнего ответа от кого-либо из основной команды. Может быть, пинг может привлечь внимание к этой проблеме :) @sebmarkbage или @gaearon

Моя команда решила полностью запретить порталы из-за этой вопиющей проблемы. Мы представляем панели с перехватчиком в контейнере, который находится в других контекстах приложения, поэтому вы получаете контексты корневого уровня бесплатно; любые другие, которые мы передаем вручную. Не очень хорошо, но лучше, чем бессмысленные обработчики событий типа «ударить крота».

Я не могу придумать каких-либо общих подходов к передаче контекста в «фальшивый портал» через реквизиты, не возвращаясь к использованию каскадных реквизитов :(

Бесчисленное количество ошибок, которые я обнаружил на 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 . Похоже, что он работает в Chrome, Firefox и Safari на OS X, но IE11 не используется из-за того, что не позволяет вручную установить event.target . Пока он заботится только о событиях Mouse, Pointer, Keyboard и Wheel. Не уверен, можно ли клонировать события перетаскивания.

К сожалению, его нельзя использовать в нашей кодовой базе, поскольку нам нужна поддержка IE11, но, возможно, кто-то другой сможет адаптировать его для собственного использования.

Что делает это особенно ошеломляющим, так это то, что поведение «по умолчанию» снова всплывает _ вниз_ по дереву компонентов. Возьмите следующее дерево:

<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 имеет к этому никакого отношения и также не собирался вообще блокировать такое поведение.

Решением для меня (если кто-то сталкивается с той же проблемой) было обычно используемое решение обертывания содержимого <Menu> в div с помощью onClick={e => e.stopPropagation()} . Но я хочу сказать, что я потерял много времени, отслеживая проблему, потому что поведение действительно не интуитивно понятно.

Решением для меня (если кто-то сталкивается с той же проблемой) было обычно используемое решение обертывания содержимого <Menu> в div с помощью onClick={e => e.stopPropagation()} . Но я хочу сказать, что я потерял много времени, отслеживая проблему, потому что поведение действительно не интуитивно понятно.

Ага - у каждого _индивидуального экземпляра проблемы_ есть одно и то же простое решение, _когда вы столкнулись с ошибкой и правильно ее определили_. Это очень крутая яма неудач, которую команда React вырубила здесь, и очень неприятно не слышать от них никакого подтверждения этого.

Я потратил несколько дней, пытаясь отладить другую проблему с неожиданным выходом mouseenter из порталов. Даже с onMouseEnter={e => e.stopPropagation()} в div портала события все еще перетекают в кнопку-владелец, как в https://github.com/facebook/react/issues/11387#issuecomment -340009465 (первый комментарий по этому вопросу). mouseenter / mouseleave вообще не должны пузыриться ...

Возможно, что еще более странно, когда я вижу пузырек синтетического события mouseenter выходящий из портала к кнопке, e.nativeEvent.type равен mouseout . React запускает синтетическое событие без восходящей цепочки на основе нативного восходящего события - и несмотря на то, что в синтетическом событии вызывается stopPropagation .

@gaearon @trueadm эта проблема уже более двух лет вызывает постоянное огромное разочарование. Эта ветка - одна из самых активных проблем React. Пожалуйста , не мог бы кто-нибудь из команды внести здесь свой вклад?

В моем случае открытие компонента Window путем нажатия кнопки заставляло окно исчезать, так как нажатие на Window вызвало нажатие кнопки, что вызвало изменение состояния

Я новичок в React, в основном использую jQuery и vanillia JS, но это сногсшибательная ошибка. Может быть около 1% случаев, когда такое поведение было бы ожидаемым ...

Мне нравятся два решения от @diegohaz , но я все же думаю, что createPortal должен иметь возможность останавливать всплытие событий.

Мой конкретный вариант использования был с обработчиками onMouseLeave и onMouseEnter всплывающей подсказки, запускаемыми потомком его дочернего портала, что было нежелательно. Собственные события исправили это, игнорируя потомков портала, поскольку они не являются потомками dom.

+1 за возможность перестать пузыриться в порталах. Было предложено просто разместить портал как родственный (а не дочерний) компоненту, от которого исходит прослушиватель событий, но я думаю, что это не работает во многих случаях использования (включая мой).

Наконец, похоже, что ReactDOM.unstable_renderSubtreeIntoContainer будет удалено , а это значит, что скоро не останется никаких разумных обходных путей для этой проблемы ...

^ помоги нам @trueadm -nobi, ты наша единственная надежда

Похоже, пинговать их на GitHub не получается 😞
Может быть, кто-нибудь с активной учетной записью в Твиттере мог бы написать об этом в Твиттере, отметив одного из участников?

Добавляю +1 к этой проблеме. В настоящее время в Notion мы используем настраиваемую реализацию портала, предшествующую React.createPortal , и вручную перенаправляем наших поставщиков контекста в новое дерево. Я попытался использовать React.createPortal но был заблокирован неожиданным всплыванием:

Предложение @sebmarkbage переместить <Portal> за пределы компонента <MenuItem> чтобы он стал родственником, решает проблему только для одного уровня вложенности. Проблема остается, если у вас есть несколько вложенных (например) пунктов меню, которые выводят подменю.

Эта проблема была автоматически помечена как устаревшая. Если эта проблема по-прежнему затрагивает вас, оставьте комментарий (например, «удар»), и мы оставим его открытым. Сожалеем, что мы еще не смогли расставить приоритеты. Если у вас есть новая дополнительная информация, включите ее в свой комментарий!

Удар.

Дэн оставил комментарий по связанной проблеме:

@mogelbrod В настоящее время мне нечего добавить к этому, но что-то вроде этого ( # 11387 (комментарий) ) кажется мне разумным, если вы переносите существующий компонент.

Продолжение Дэна в том же номере :

Спасибо за контекст об обходном пути. Поскольку у вас уже есть знания в этой предметной области, лучшим следующим шагом, вероятно, будет написание RFC для желаемого поведения и рассмотренных вами альтернатив: https://github.com/reactjs/rfcs. Имейте в виду, что RFC, в котором говорится «давайте просто изменим это», вряд ли будет полезным. Написание хорошего RFC требует как понимания того, почему у нас есть текущее поведение, так и плана по его изменению таким образом, чтобы он соответствовал вашим вариантам использования, не отвлекаясь от других.

Несмотря на это , unstable_renderSubtreeIntoContainer не поддерживается, так что давайте разберемся в этих двух обсуждениях. Мы не будем добавлять к нему распространение контекста, потому что весь 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 / 150 , https://github.com/reactjs/rfcs/pull/118 , https://github.com/reactjs/rfcs/pull/109 , https://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 (например, изоморфные идентификаторы).

Я очень рад услышать, что вы действительно посмотрите на другие RFC и что они вносят свой вклад в разработку функций, но «На нас повлияли эти документы вне RFC, хотя мы никогда их не комментировали», я думаю, это иллюстрирует мою точку зрения, а не бросить вызов этому.

Другими словами, мы рассматриваем RFC отчасти как механизм исследования сообщества.

Это супер разумно, но не то, что в репозитории RFC говорится о его подходе, и не о том, как другие люди обычно думают о RFC. Процесс RFC обычно является связующим звеном и точкой взаимодействия между командой и сообществом, а также чем-то вроде равного игрового поля с точки зрения рассмотрения функций и процесса.

Оставим в стороне более важные моменты об управлении сообществом. Просить людей тратить время на написание подробных предложений, а затем защищать их от других сторонних участников, в то время как команда реагирования встречает молчание, обескураживает и активно усиливает впечатление, что FB заботится только о собственных потребностях в OSS. Это воняет, потому что я знаю, что ты так не будешь чувствовать или проектировать.

Если процесс RFC должен быть таким: «вот где вы можете изложить свои проблемы и варианты использования, и мы их прочитаем, когда / если мы достигнем точки, в которой мы сможем реализовать эту функцию». Честно говоря, это хороший подход. Я думаю, что сообщество выиграет от того, что это будет четко прописано, иначе ppl будет (и будет) предполагать такой же уровень вовлеченности и участия, который часто имеют другие RFC-процессы, а затем будет активно обескуражен, когда это не сработает. У меня определенно сложилось такое впечатление, даже если я немного более проницателен, чем другие участники.

Конечно, я думаю, что согласен со всем этим. Я не хочу превращать это в мета-поток, но просто говорю, что, поскольку люди продолжают пинговать об этом потоке, наиболее действенный способ продвинуть его вперед - это написать предложение о том, как это должно работать, которое принимает во внимание обе стороны. сторон во внимание и составляет глубокое понимание проблемного пространства . Я полностью понимаю, если это не то, в чем люди хотели бы вцепиться (частично в зависимости от того, как мы реагируем на RFC), поэтому я не предлагал этого раньше, но, поскольку я продолжаю получать личные пинги, я хотел предложить, что как вариант для тех, кто мотивирован.

честно говоря, это не подходящее место для получения мета на RFC :)

@gaearon - это шестой по количеству голосов открытый вопрос, открытый в React, и четвертый по количеству комментариев. Он был открыт с момента выхода React 16, и ему осталось всего 2 месяца до 3 лет. Тем не менее, основная команда React очень мало взаимодействовала. Очень пренебрежительно говорить, что «сообщество должно предложить решение» после того, как прошло и произошло столько времени и боли. Пожалуйста, поймите, что, хотя у него есть несколько очень полезных приложений, такое поведение по умолчанию было ошибкой дизайна. Сообщество не должно решать RFC, чтобы исправить это.

Я сожалею, что комментирую эту проблему, и отказываюсь от своего предложения по поводу RFC сообщества. Ты прав, наверное, это плохая идея. Я должен добавить, что эта проблема приобрела очень эмоциональный характер, и мне лично, как человеку, трудно заниматься ею - хотя я понимаю, что это важно, и многие люди сильно к этому относятся.

Позвольте мне кратко ответить о состоянии этой ветки.

Во-первых, я хочу извиниться перед людьми, которые комментировали и были разочарованы тем, что мы не продолжаем дальнейшие обсуждения в этой беседе. Если бы я читал этот выпуск со стороны, у меня, вероятно, сложилось бы впечатление, что команда React совершила ошибку, не желает ее признавать и готова принять простое решение («просто добавьте одно логическое значение, насколько сложно да будет! ») более двух лет, потому что им наплевать на сообщество. Я прекрасно понимаю, как вы пришли к такому выводу.

Я знаю, что за этот вопрос очень проголосовали. Это неоднократно поднималось в этой теме, возможно, с той точки зрения, что, если бы команда React знала, что это большая проблема, мы бы решили ее раньше. Мы знаем, что это проблема - люди регулярно сообщают нам об этом в частном порядке или приводят это в качестве примера того, как команда React не заботится о сообществе. Хотя я полностью признаю, что молчание вызывает разочарование, растущее давление с целью «просто сделать что-нибудь» затрудняет продуктивное решение этой проблемы.

У этой проблемы есть обходные пути, что отличает ее от уязвимости системы безопасности или сбоя, с которыми необходимо срочно бороться. Мы знаем, что обходные пути работают (но не идеальны и могут раздражать), потому что мы используем некоторые из них сами, особенно в коде, который был написан до React 16. Я думаю, мы можем согласиться с тем, что, хотя эта проблема явно разочаровывала многих количество людей, это все еще относится к другому классу проблем, чем сбой или проблема безопасности, на которую необходимо реагировать в течение определенного периода времени.

Кроме того, я не согласен с утверждением, что есть простое решение, которое мы можем реализовать завтра. Даже если мы считаем первоначальное поведение ошибкой (с чем я не уверен, что я согласен), это означает, что планка того, что следующее поведение будет обрабатывать все варианты использования, еще выше . Если мы исправим одни случаи, но сломаем другие, мы не добьемся никакого прогресса и создадим массу оттока. Имейте в виду, что мы не услышим о тех случаях, когда текущее поведение хорошо работает в этой проблеме. Мы узнаем об этом только после того, как сломаем его.

Приведу пример: текущее поведение на самом деле действительно полезно для сценария использования управления с декларативной фокусировкой, который мы исследовали в течение довольно долгого времени. Полезно рассматривать фокус / размытие как происходящие «внутри» модального окна по отношению к дереву деталей, несмотря на то, что это портал. Если бы мы отправили «простое» предложение createPortal(tree, boolean) предложенное в этом потоке, этот вариант использования не работал бы, потому что сам портал не может «знать», какое поведение мы хотим. Любое исследование возможного решения требует рассмотрения десятков вариантов использования, а некоторые из них еще даже полностью не изучены. Это обязательно нужно сделать в какой-то момент, но это также требует огромного количества времени, чтобы сделать это правильно, и пока мы не можем сосредоточиться на этом.

События, в частности, являются сложной областью, например, мы только что внесли ряд изменений, направленных на решение проблем, возникших в течение многих лет, и в этом году этому уделялось большое внимание. Но мы можем делать очень много вещей одновременно.

Как правило, мы, как команда, стараемся глубоко сосредоточиться на нескольких проблемах, а не на многих проблемах поверхностно. К сожалению, это означает, что некоторые концептуальные недостатки и пробелы могут не заполняться годами, потому что мы находимся в процессе исправления других важных пробелов или не имеем разработанной альтернативной конструкции, которая решила бы проблему навсегда. Я знаю, что это неприятно слышать, и отчасти поэтому я держался подальше от этой беседы. Некоторые другие похожие темы превратились в более подробные объяснения проблем и возможных решений, которые полезны, но эта в основном превратилась в поток «+1» и предложений по «простому» исправлению, поэтому это было сложно. заниматься этим осмысленно.

Я знаю, что это не тот ответ, который люди хотели услышать, но я надеюсь, что это лучше, чем вообще не отвечать.

Еще одна вещь, на которую стоит обратить внимание, - это то, что некоторые из болевых точек, описанных в этой беседе, могли быть решены другими способами. Например:

В частности: вызов stopPropagation для события синтетического фокуса, чтобы предотвратить его выход из портала, приводит к тому, что stopPropagation также вызывается в собственном событии фокуса в захваченном обработчике React на #document, что означает, что он не попал в другой захваченный обработчик на

React больше не использует фазу захвата для имитации восходящей цепочки, а также больше не слушает события в документе. Так что, не отказываясь от разочарования, определенно необходимо будет переоценить все, что было опубликовано до сих пор, в свете других изменений.

Собственные события все еще пузырились, и, поскольку наш код React размещен внутри приложения, в основном использующего jQuery, глобальный обработчик jQuery keyDown на

все еще получает событие.

Точно так же React 17 будет прикреплять события к корням и контейнерам портала (и фактически останавливать собственное распространение в этой точке), поэтому я тоже ожидал решения.

По поводу пунктов об удалении renderSubtreeIntoContainer . Буквально его единственное отличие от ReactDOM.render состоит в том, что он распространяет устаревший контекст. Поскольку любой выпуск, который не будет включать renderSubtreeIntoContainer , также не будет включать устаревший контекст, ReactDOM.render останется на 100% идентичной альтернативой. Это, конечно, не решает более широкой проблемы, но я думаю, что озабоченность по поводу renderSubtree частности, несколько неуместна.

@gaearon

По поводу пунктов об удалении renderSubtreeIntoContainer . Буквально его единственное отличие от ReactDOM.render состоит в том, что он распространяет устаревший контекст. Поскольку любой выпуск, который не будет включать renderSubtreeIntoContainer , также не будет включать устаревший контекст, ReactDOM.render останется на 100% идентичной альтернативой. Это, конечно, не решает более широкой проблемы, но я думаю, что озабоченность по поводу renderSubtree частности, несколько неуместна.

Теперь, когда вы упомянули об этом, мне интересно, будет ли приведенный ниже код допустимой и безопасной реализацией для портала React без всплытия событий:

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=/src/App.js

По-прежнему существует проблема с пропуском через Modern Context, но это не новая проблема (она также влияет на renderSubtree ). Обходной путь состоит в том, чтобы окружить ваше дерево кучей поставщиков контекста. В целом, вложение деревьев не идеально, поэтому я бы не рекомендовал переходить к этому шаблону ни в чем другом, кроме устаревших сценариев существующего кода.

Еще раз большое спасибо за рецензию @gaearon!

Похоже, что объединение списка неработающих случаев + обходные пути (обновлено для React v17) было бы наиболее продуктивным для кого-то, не входящего в основную команду (поправьте меня, если я ошибаюсь!).

Я загружен в ближайшие недели, но постараюсь сделать это как можно скорее. Если кто-то еще сможет сделать это раньше или присоединиться к вам с помощью сниппетов (как только что сделал

Составление списка кейсов определенно было бы полезно, хотя я бы сказал, что он должен включать не только неработающие кейсы, но и те случаи, когда текущее поведение имеет смысл.

Если есть общедоступное пространство, которое можно добавить, я был бы счастлив добавить варианты использования как из наших приложений, так и в качестве автора библиотеки пользовательского интерфейса. В целом я согласен с Дэном в том, что, хотя иногда это раздражает, его легко обойти. В случаях, когда вы действительно хотите пузыриться в React, очень сложно прикрыть случай без помощи React.

Составление списка кейсов определенно было бы полезно, хотя я бы сказал, что он должен включать не только неработающие кейсы, но и те случаи, когда текущее поведение имеет смысл.

Я был бы счастлив включить их, если кто-нибудь может указать мне на какой-то открытый исходный код / ​​извлеченный код, который полагается на него! Как вы упомянули ранее, это немного сложно найти, поскольку в решении этой проблемы участвуют только люди, у которых есть проблемы с текущим поведением 😅

Если есть общедоступное пространство, которое можно добавить, я был бы счастлив добавить варианты использования как из наших приложений, так и в качестве автора библиотеки пользовательского интерфейса. В целом я согласен с Дэном в том, что, хотя иногда это раздражает, его легко обойти. В случаях, когда вы действительно хотите пузыриться в React, очень сложно прикрыть случай без помощи React.

Любое конкретное пространство, которое вы имеете в виду, или совместное использование одного кода (или jsfiddle и т. Д.) Для каждого случая сработает в качестве стартера? Я могу попробовать собрать их все, когда мы соберем несколько кейсов.

Я начал тему здесь: https://github.com/facebook/react/issues/19637. Давайте сосредоточимся на практических примерах, а этот оставим для общего обсуждения.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги