我们相信:世界是美好的,你是我也是。平行空间的世界里面, 不同版本的生活 也在继续...

首先, react 可不可以主动卸载一个根组件?肯定是可以的, react 必然有这个能力从根源上卸载。那换成卸载子组件呢?那么,也必须是有这个能力的。那么,如何卸载这个根组件或者子组件呢?这就是本文中要探讨的问题。

苏南大叔:react教程,如何卸载一个根组件或子组件?root.unmount() - root-unmount
react教程,如何卸载一个根组件或子组件?root.unmount()(图6-1)

苏南大叔的程序如此灵动博客,记录苏南大叔和计算机代码的故事。测试环境: create-react-app@5.0.1 react@18.2.0 react-dom@18.2.0 node@16.14.2

测试标的组件

苏南大叔的目标是这样的:
1、使用 react 的方式卸载组件,而不是通过 html 操作 dom 的方式来实现这个需求。
2、卸载组件的时候,希望能够把关联的 dom 元素也一并卸载。比如利用传送门脱离原有体系的组件。

苏南大叔:react教程,如何卸载一个根组件或子组件?root.unmount() - 测试组件
react教程,如何卸载一个根组件或子组件?root.unmount()(图6-2)

测试组件:

<>
  <div className='class' ref={this.nodeRef}>带class的组件</div>
  {this.state.show.a1 && <p>普通组件a1</p>}
  {this.state.show.a2 && (<div onClick={this.handleClick}>
    传送门组件外部a2
    <Modal>
      <button>传送门组件内部</button>
    </Modal>
  </div>)}
</>

有关传送门组件的定义:
https://newsn.net/say/react-portal.html

方案一, .parentNode.removeChild()

首先需要肯定的是: react 最终也是在页面上形成了传统的元素节点。所以,使用 .removeChild 的方式,是必然可以删除节点的。但是,这操作超出了 react 的概念。附加在被删除节点上的 ref 或者 state 之类的数据,可能会造成一些问题。

let _node = document.querySelector('.class');
_node.parentNode.removeChild(_node);

或者

let _node = this.nodeRef.current;
_node.parentNode.removeChild(_node);

原理上就是使用真实节点的父节点来删除对应真实节点。所以,它必然可以生效,但是也必然必能关联删除附加节点(比如传送门生成的节点)。

苏南大叔:react教程,如何卸载一个根组件或子组件?root.unmount() - 方案一
react教程,如何卸载一个根组件或子组件?root.unmount()(图6-3)

方案二, root.unmount() / root.render("")

这个方案仅仅针对根组件 root 生效,是无法控制子组件的。而 root 一般是通过 props.root 从最外层传递进来的变量,例如:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App root={root} />
);

这样的话,在 <App/> 内部,就可以通过 props.root 来进行整体组件卸载了。两种方式:

props.root.unmount();

或者

props.root.render("");

苏南大叔:react教程,如何卸载一个根组件或子组件?root.unmount() - 方案二
react教程,如何卸载一个根组件或子组件?root.unmount()(图6-4)

ReactDOM.unmountComponentAtNode(真实dom) ,在最新的 react@18 系列中已经不存在这个函数了。

方案三, .createRoot().unmount() / .createRoot().render("")

这个方案可以处理子组件,但是不能处理根组件 root 。原理上来说,上一个方案中的 root 是已经被 .createRoot() 过的。已经被处理过的 .createRoot() 不能再次被处理,这也是 root 不能使用本方案的原因。

这个方案原理就是把子组件的真实 dom ,变成一个 react 的根组件,然后再 unmount() 或者 render("") ,达到卸载子组件的目的。现在假设真实 dom 是个 className aaa 的子组件。

ReactDOM.createRoot(document.querySelector('.aaa')).unmount();

或者

ReactDOM.createRoot(document.querySelector('.aaa')).render("");

苏南大叔:react教程,如何卸载一个根组件或子组件?root.unmount() - 方案三
react教程,如何卸载一个根组件或子组件?root.unmount()(图6-5)

虽然这个方案能够达到目的,但是苏南大叔总是感觉哪里不对劲。这个函数的结果并不像是对原来的组件进行卸载,而且对原结构进行了破坏...

方案四, state && <组件/> 【推荐】

苏南大叔认为: react 和传统的页面逻辑的区别就在于:行动的主动权在谁手里。不同于传统页面, react 更像是一种自发式的组织形式,并没有谁能够主动掌控组件的生死。所以从 react 的方式来思考问题的话,主动卸载某个组件的函数是不是理论上不存在啊...

那么,以 react 的思路来解释这个需求的话。苏南大叔认为:完美方案就是 state && <组件/> 。当不想要这个组件的时候,就切换一下 state 即可。对应组件就会被卸载,而不是隐藏。

this.state = { show: { a1: true, a2: true } }
{this.state.show.a1 && <p>a1</p>}
{this.state.show.a2 && (<div>
  <modal>content</modal>
</div>)}
let _next_show = { ...this.state.show, ...{ a1: false } };
this.setState({ show: _next_show });

苏南大叔:react教程,如何卸载一个根组件或子组件?root.unmount() - 方案四
react教程,如何卸载一个根组件或子组件?root.unmount()(图6-6)

测试代码

以下是本文的完整测试代码:

import React, { createRef } from 'react';
import ReactDOM from 'react-dom/client';
import ReactDOMPortal from 'react-dom';
const root2 = document.getElementById('root-modal');
class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  componentDidMount() {
    root2.appendChild(this.el);
  componentWillUnmount() {
    root2.removeChild(this.el);
  render() {
    return ReactDOMPortal.createPortal(
      this.props.children,
      this.el
class App extends React.Component {
  constructor(props) {
    super(props);
    this.root = props.root;
    this.handleClick = this.handleClick.bind(this);
    this.nodeRef = createRef(null);
    this.state = { show: { a1: true, a2: true } }
  handleClick() {
    // document.querySelector('.class').parentNode.removeChild(document.querySelector('.class'));
    // this.nodeRef.current.parentNode.removeChild(this.nodeRef.current);
    // this.root.unmount();
    // this.root.render("");
    // ReactDOM.unmountComponentAtNode(this.nodeRef.current);
    // ReactDOM.createRoot(document.querySelector('.class')).unmount();
    // ReactDOM.createRoot(document.querySelector('.class')).render("");
    let _next_show = { ...this.state.show, ...{ a2: false } };
    this.setState({ show: _next_show });
  render() {
    return (
        <div className='class' ref={this.nodeRef}>带class的组件</div>
        {this.state.show.a1 && <p>普通组件a1</p>}
        {this.state.show.a2 && (<div onClick={this.handleClick}>
          传送门组件外部a2
          <Modal>
            <button>传送门组件内部</button>
          </Modal>