Задний план:
Я пытаюсь найти лучший способ реализовать компонент портала, который обертывает собственную утилиту портала React. Компонент просто обработает создание корневого элемента портала, безопасно вставит его в DOM, отобразит в нем любой из дочерних элементов компонента, а затем снова безопасно удалит его из DOM по мере размонтирования компонента.
Проблема:
React настоятельно рекомендует избегать побочных эффектов (таких как манипулирование DOM) за пределами безопасных методов жизненного цикла React (componentDidMount, componentDidUpdate и т. д.), поскольку это может вызвать проблемы (утечки памяти, устаревшие узлы и т. д.). В примерах использования порталов React они монтируют корневой элемент портала в дерево DOM на componentDidMount, но, похоже, это вызывает другие проблемы.
Выпуск №1:
Если компонент портала «портирует» своих дочерних элементов в созданный корневой элемент во время его метода рендеринга, но ждет, пока сработает его метод componentDidMount, прежде чем добавить этот корневой элемент в дерево DOM, то любой из дочерних элементов портала, которым требуется доступ к DOM во время их собственные методы жизненного цикла componentDidMount будут иметь проблемы, поскольку в этот момент они будут смонтированы на отсоединенном узле.
Этот проблема позже был рассмотрен в документации React, в которой рекомендуется установить для свойства mounted значение true в состоянии компонента портала после того, как компонент портала завершил монтирование и успешно добавил корневой элемент портала в дерево DOM. Затем при рендеринге вы можете отложить рендеринг любого из дочерних элементов портала до тех пор, пока для этого свойства монтирования не будет установлено значение true, поскольку это гарантировало бы, что все эти дочерние элементы будут визуализированы в фактическом дереве DOM до их собственных соответствующих методов жизненного цикла componentDidMount. выстрелил бы. Здорово. Но это приводит нас к...
Выпуск №2:
Если ваш компонент портала откладывает рендеринг любого из своих дочерних элементов до тех пор, пока он сам не смонтируется, то любой из методов жизненного цикла componentDidMount его предков также сработает до того, как любой из этих дочерних элементов будет смонтирован. Таким образом, у любого из предков компонента портала, которым требуется доступ к ссылкам на любой из этих дочерних элементов во время их собственных методов жизненного цикла componentDidMount, возникнут проблемы. Я еще не придумал хороший способ обойти это.
Вопрос:
Существует ли чистый способ безопасно реализовать компонент портала, чтобы его дочерние элементы имели доступ к DOM во время своих методов жизненного цикла componentDidMount, а также позволяли предкам компонента портала иметь доступ к ссылкам на этих дочерних элементах в течение их собственного соответствующего жизненного цикла componentDidMount методы?
Код ссылки:
import { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
export default class Portal extends Component {
static propTypes = {
/** This component uses Portals to dynamically render it's contents into
* whatever DOM Node contains the **id** supplied by this prop
* ('portal-root' by default). If a DOM Node cannot be found with the
* specified **id** then this component will create one and append it
* to the 'Document.Body'. */
rootId: PropTypes.string
};
static defaultProps = {
rootId: 'portal-root'
};
constructor(props) {
super(props);
this.state = { mounted: false };
this.portal = document.createElement('div');
}
componentDidMount() {
this.setRoot();
this.setState({ mounted: true });
}
componentDidUpdate( prevProps, prevState ) {
if ( this.props.rootId !== prevProps.rootId ) this.setRoot();
}
componentWillUnmount() {
if ( this.root ) {
this.root.removeChild(this.portal);
if ( !this.root.hasChildNodes() ) this.root.parentNode.removeChild(this.root);
}
}
render() {
this.portal.className = this.props.className ? `${this.props.className} Portal` : 'Portal';
return this.state.mounted && ReactDOM.createPortal(
this.props.children,
this.portal,
);
}
setRoot = () => {
this.prevRoot = this.root;
this.root = document.getElementById(this.props.rootId);
if (!this.root) {
this.root = document.createElement('main');
this.root.id = this.props.rootId;
document.body.appendChild(this.root);
}
this.root.appendChild(this.portal);
if ( this.prevRoot && !this.prevRoot.hasChildNodes() ) {
this.prevRoot.parentNode.removeChild(this.prevRoot);
}
}
}



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


constructor — это допустимый метод жизненного цикла, в котором вы можете выполнять побочные эффекты. Нет причин, по которым вы не можете создать/прикрепить корневой элемент в конструкторе:
class Portal extends Component {
constructor(props) {
super();
const root = document.findElementById(props.rootId);
this.portal = document.createElement('div');
root.appendChild(portal);
}
componentWillUnmount() {
this.portal.parent.removeChild(this.portal);
}
render() {
ReactDOM.createPortal(this.props.children, this.portal);
}
// TODO: add your logic to support changing rootId if you *really* need it
}
В проблеме, на которую я ссылался выше, Бвон из основной команды React упомянул следующее: «Побочные эффекты (например, изменение DOM) безопасны только в методах componentDidMount, componentDidUpdate и componentWillUnmount». Я предполагаю, что исключает конструктор. Однако, если нет другого решения, это, вероятно, будет путь, который я в конечном итоге выберу. Хотя для меня было бы неожиданностью, если бы только чистое решение считалось небезопасным.
Истинный. Возможно, вам придется выбрать свой яд.
@Spencer Вы правы, спасибо, теперь документы здесь явно предлагает обходной путь: «добавить состояние и визуализировать дочерние элементы только при вставке в дерево DOM». Итак, мое решение заключалось в использовании this.setState({ mounted: true }) в componentDidMount и условном рендеринге внутри портала с использованием этого флага в состоянии.
@lorenzo-s Это подход, который я использую в своем справочном коде выше. Если что-то не изменилось за последние два года, это приведет к другой проблеме, которую я пытался объяснить в заголовке Выпуска 2 выше. В качестве примера предположим, что у вас есть компонент, который отображает элемент ввода и использует ссылку на этот ввод, чтобы дать ему фокус во время его собственного метода жизненного цикла componentDidMount. Обычно это работало бы нормально, но если вы затем обернете этот ввод компонентом портала, на который я ссылался выше, вы столкнетесь с проблемой, потому что... (продолжение)
(продолжение)... Родительский метод жизненного цикла componentDidMount сработает до того, как элемент ввода будет фактически обработан, поэтому ссылка будет недействительной. В обычном потоке React дочерние элементы монтируются раньше своих родителей, но это позволяет обойти это путем монтирования сначала родителей, а затем дочерних элементов, что предотвращает любой доступ к этим дочерним элементам во время цикла монтирования их родителей. Это накладывает ограничения на возможность повторного использования такого компонента и может вызвать тонкие проблемы, которые может быть трудно найти и отладить.
@ Спенсер Ты прав. Я ушел таким образом, и через несколько дней я столкнулся именно с той проблемой, которую вы описываете (акцент на componentDidMount)... В итоге я отменил изменение и включил его в выделенной дополнительной опоре моего компонента портала... Мех, но все же...
Ну, я могу ошибаться, и, пожалуйста, поправьте меня, если я ошибаюсь, но я думаю, что в документах React где-то упоминается, что componentWillUnmount гарантированно сработает только в том случае, если ранее был запущен componentDidMount. Это означает, что вполне возможно, что компонент будет удален до завершения цикла монтирования, и в таких случаях нет гарантии, что componentWillUnmount когда-либо сработает. Если это так, то любые узлы dom, добавленные в конструктор, не гарантированно будут очищены, если компонент будет удален до его монтирования, что оставляет возможность устаревших узлов.