【问题标题】:ref issues due to React Portals and componentDidMount由于 React Portals 和 componentDidMount 导致的 ref 问题
【发布时间】:2019-02-05 18:15:32
【问题描述】:

背景:
我试图找出实现包装 React 的本机门户实用程序的门户组件的最佳方法。该组件将简单地处理创建门户的根元素,将其安全地插入 DOM,将组件的任何子元素渲染到其中,然后在卸载组件时再次从 DOM 中安全地删除它。

问题:
React 强烈建议不要在 React 的安全生命周期方法(componentDidMount、componentDidUpdate 等)之外产生副作用(如操作 DOM),因为这可能会导致问题(内存泄漏、陈旧节点等)。在 React 关于如何使用 Portal 的示例中,他们将 Portal 的根元素挂载到 componentDidMount 上的 DOM 树中,但这似乎会导致其他问题。

问题编号 1:
如果 Portal 组件 'portals' 在它的渲染方法期间它是创建的根元素的子元素,但在将根元素附加到 DOM 树之前等待它的 componentDidMount 方法触发,那么任何需要在其期间访问 DOM 的门户的子元素自己的 componentDidMount 生命周期方法会有问题,因为那时它们将被挂载到一个分离的节点。 这个issue 后来在 React 的文档中得到解决,该文档建议在 Portal 组件完成安装并成功将门户根元素附加到 DOM 树后,在 Portal 组件的状态上将“mounted”属性设置为 true。然后在渲染中,您可以推迟渲染 Portal 的任何子级,直到该 mount 属性设置为 true,因为这将保证所有这些子级将在其各自的 componentDidMount 生命周期方法之前渲染到实际的 DOM 树中会开火。伟大的。但这导致我们...

问题编号 2:
如果您的 Portal 组件推迟渲染它的任何子组件,直到它本身已挂载,则其祖先的任何 componentDidMount 生命周期方法也将在任何这些子组件被挂载之前触发。因此,任何 Portal 组件的祖先在他们自己的 componentDidMount 生命周期方法中需要访问任何这些子项的 refs 都会有问题。我还没有找到解决这个问题的好方法。

问题:
是否有一种干净的方法可以安全地实现门户组件,以便其子组件在其 componentDidMount 生命周期方法期间可以访问 DOM,同时还允许门户组件的祖先在其各自的 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 reactjs


    【解决方案1】:

    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 的文档在某处提到 componentWillUnmount 只有在 componentDidMount 之前被触发时才能保证触发。这意味着组件完全有可能在完成安装周期之前被移除,在这种情况下,无法保证 componentWillUnmount 会触发。如果是这种情况,那么如果组件在挂载之前被移除,那么在构造函数中添加的任何 dom 节点都不能保证被清理,从而留下过时节点的可能性。
    • 在我上面链接的问题中,来自 React 核心团队的 bvaughn 提到了以下内容:“副作用(如修改 DOM)仅在 componentDidMountcomponentDidUpdatecomponentWillUnmount 方法中是安全的。”我假设不包括构造函数。如果没有其他解决方案,那很可能是我最终要走的路。虽然如果只有干净的解决方案被认为是不安全的解决方案,我会感到惊讶。
    • 是的。你可能不得不选择你的毒药。
    • @lorenzo-s 这就是我在上面的参考代码中采用的方法。除非过去两年发生了一些变化,否则这将导致我试图在上面的问题 2 标题下解释的不同问题。例如,假设您有一个组件,它呈现一个输入元素并使用该输入的 ref 在它自己的 componentDidMount 生命周期方法期间为其提供焦点。通常这可以正常工作,但如果你用我上面提到的门户组件包装该输入,你会遇到问题,因为...(续)
    • (续)... Parent 的 componentDidMount 生命周期方法将在输入元素实际呈现之前触发,因此 ref 将无效。在正常的 React 流程中,孩子在他们的父母之前被安装,但这通过先安装父母然后是孩子来规避这一点,这会阻止在他们父母的安装周期中访问这些孩子。这限制了此类组件的可重用性,并有可能导致难以发现和调试的细微问题。
    猜你喜欢
    • 1970-01-01
    • 2022-08-03
    • 2013-04-04
    • 2011-07-10
    • 2018-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-27
    相关资源
    最近更新 更多