如果父组件发生变化,防止子组件卸载和重新安装

IT技术 javascript reactjs
2021-04-29 04:21:19

问题:

在下面的代码中,当Toggle ParentA/B单击按钮<Child />组件将被卸载并再次重新安装,但实际上只有Parent组件发生了变化。

Child在这种情况下如何防止卸载和重新安装?

import React, { useState } from "react"

const App = () => {
    const [a, setA] = useState(true)
    return (
        <div>
            <button onClick={() => setA(!a)}>Toggle ParentA/B</button>
            {a ? <ParentA><Child /></ParentA> : <ParentB><Child /></ParentB>}
        </div>
    )
}
const ParentA = ({ children }) => <div>parentA{children}</div>
const ParentB = ({ children }) => <div>parentB{children}</div>

class Child extends React.Component {
    componentDidMount() {
        console.log('child did mount')
    }

    componentWillUnmount() {
        console.log('child will unmount')

    }

    render() {
        return (
            <div>child</div>
        )
    }

}

export default App

对潜在答案的回复:

为什么不让 Child 组件重新挂载,因为它只是一个<div>元素?

通常,如果Child组件的渲染成本低,这无关紧要但是,在我的情况下,Child组件需要很长时间才能安装,因此每次Parent组件更改时我都无法重新安装

您可以将parentAorparentB字符串作为props传递给通用父组件。

const App = () => {
    const [a, setA] = useState(true)
    return (
        <div>
            <button onClick={() => setA(!a)}>Toggle ParentA/B</button>
            <ParentGeneric content={a? 'parentA': 'parentB'}><Child /></ParentGeneric>
        </div>
    )
}
const ParentGeneric = ({children, content}) => <div>{content}{children}</div>

class Child extends React.Component {
    ...
}

这会奏效。不幸的是,这限制了我的Parent组件的 JSX 结构是相同的。换句话说,如果 myParentAParentB的 JSX 结构不同,那么我不确定如何将差异作为props传递。

例如:

const ParentA = ({ children }) => <div><div><h1>parentA{children}</h1></div></div>
const ParentB = ({ children }) => <div>parentB{children}</div>

如果这样定义 parentA 和 parentB,是否仍然可以将这两个组件抽象为一个ParentGeneric组件并简单地将ParentA之间的结构差异ParentB作为props传递

4个回答

- 编辑 -

我的以下解决方案似乎具有误导性。我现在同意该线程中的其他人的意见,即无法实现问题中所要求的行为。我对原始答案所做的一切都是阻止安装/卸载运行。尽管如此,每次更改上层布局组件时,父组件的全部内容都会被丢弃并从头开始重新渲染。

——编辑结束——

有一种方法!

确实,React 的协调算法总是会挂载和卸载其父母被替换的孩子(不同的父母类型导致整个家庭分支从更改的父母开始重新挂载)。

但是,您仍然可以将“子”组件显示为某个父布局组件的子组件,并且在父类型更改时不会挂载/卸载。你只需要改变组件树的顺序,利用渲染props模式:

  1. 子(重)组件首先呈现。
  2. Child 获得一个组件 prop,它将成为父布局组件。
  3. Child 然后从自己周围的props渲染它的父级,将自己设置为它的父级的子级。
  4. 父组件是布局组件,无论您选择何时何地都可以渲染它的子组件!

示例,基于您提供的代码:

import { useState, Component } from "react";

const App = () => {
  const [a, setA] = useState(true);
  return (
    <div>
      <button onClick={() => setA((prev) => !prev)}>Toggle ParentA/B</button>
      <Child layout={a ? ParentA : ParentB} />
    </div>
  );
};
const ParentA = ({ children }) => <div>parentA{children}</div>;
const ParentB = ({ children }) => <div>parentB{children}</div>;

class Child extends Component {
  componentDidMount() {
    console.log("child did mount");
  }

  componentWillUnmount() {
    console.log("child will unmount");
  }

  render() {
    return (
      <this.props.layout>
        <div>child</div>
      </this.props.layout>
    );
  }
}

export default App;

工作 CodeSandbox:https ://codesandbox.io/s/change-parent-no-unmounted-child-nnek0 ? file =/ src/App.js

检查控制台,它可以工作,并且是可以接受的做法。

这是不可能的,这里有明确的定义(强调我的):

这个算法问题有一些通用的解决方案,即生成将一棵树转换为另一棵树的最少操作数。然而,最先进的算法的复杂度为 O(n 3 ),其中 n 是树中元素的数量。

如果我们在 React 中使用它,显示 1000 个元素将需要十亿次比较。这太贵了。相反,React 基于两个假设实现了启发式 O(n) 算法:

  1. 不同类型的两个元素将产生不同的树。

  2. 开发人员可以通过keyprop提示哪些子元素可以在不同的渲染中保持稳定

在实践中,这些假设几乎适用于所有实际用例。

如果 React 会区分不同父类型的子代,那么整个调解过程就会大幅减慢。

所以你只剩下你提出的带有props的通用父级的解决方案。

这对技术答案没有太大影响,但有一些重新命名的库可以实现这一点:react-reverse-portalreact-reparenting

我最近自己研究了它,但它们有点实验性,似乎需要担心很多开销。如果您实际上并不需要跨多个分支移动组件,那么来自@deckele 的答案看起来是一个更好的解决方案。

如果不了解更多关于这个问题的背景,以及你试图实现的更大目标,就很难提供特定的解决方案。

首先,由于 React 和 DOM 的性质,不可能阻止子项卸载和再次挂载。如果您希望 DOM 节点留在那里,那么树结构必须保持不变。

通过使用这个

{a ? <ParentA><Child /></ParentA> : <ParentB><Child /></ParentB>}

每次切换激活时,您实际上都在更改树中该级别的 DOM 树,即您正在删除和替换节点,根据定义,这意味着卸载和安装。

为了避免挂载和卸载子项,您必须在每次渲染时保持 DOM 树结构相同,至少在树中与子项所在的位置一样深。

您可以通过简单地根据切换更改父组件的可见性,然后使用样式适当地定位容器来实现您想要效果,请参阅https://codesandbox.io/s/adoring-hawking-61y66 ?file=/src/App.js

根据您实际想要实现的目标,您可以将Child组件作为不同的实例或相同的实例。如果您想要相同的实例,则可以childInstance直接在渲染函数中使用将子项定义为 JSX。您会从控制台看到子项不会在每次切换时挂载和卸载——因为 DOM 树没有被改变!