在下面的代码中,单击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
组件更改时,我都无法进行重新安装。
您可以将
parentA
或parentB
字符串作为道具传递给通用的Parent组件。
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结构是相同的。换句话说,如果我的ParentA
和ParentB
的JSX结构不同,那么我不确定如何作为道具传递差异。
例如:
const ParentA = ({ children }) => <div><div><h1>parentA{children}</h1></div></div>
const ParentB = ({ children }) => <div>parentB{children}</div>
如果parentA和parentB以此方式定义,是否仍然有可能将这两个组件抽象为ParentGeneric
组件,并简单地传递ParentA
和ParentB
之间的结构差异作为支持?
答案 0 :(得分:6)
-- 编辑 --
看来我的以下解决方案具有误导性。我现在同意该线程中的其他人的意见,即无法实现问题中所要求的行为。我对原始答案所做的一切都是阻止安装/卸载运行。尽管如此,每次更改上层布局组件时,父组件的全部内容都会被丢弃并从头开始重新渲染。
-- 编辑结束--
有办法!
确实,React 的 reconciliation algorithm 总是会挂载和卸载已替换其父代的子代(不同的父代类型会导致整个家族分支从更改的父代开始重新挂载)。
但是,您仍然可以将“子”组件显示为某个父布局组件的子组件,并且在父类型更改时不会挂载/卸载。你只需要改变组件树的顺序,利用渲染道具模式:
示例,基于您提供的代码:
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;
工作代码沙盒:https://codesandbox.io/s/change-parent-no-unmounted-child-nnek0?file=/src/App.js
检查控制台,它可以工作,并且是可以接受的做法。
答案 1 :(得分:3)
这是不可能的,而且定义明确here(强调我的):
<块引用>这个算法问题有一些通用的解决方案,即生成将一棵树转换为另一棵树的最少操作数。但是,state of the art algorithms 的复杂度为 O(n3),其中 n 是树中元素的数量。
如果我们在 React 中使用它,显示 1000 个元素将需要十亿次比较。这太贵了。相反,React 基于两个假设实现了启发式 O(n) 算法:
不同类型的两个元素会产生不同的树。
开发人员可以使用 key
属性提示哪些子元素在不同的渲染中可能是稳定的。
在实践中,这些假设几乎适用于所有实际用例。
如果 React 会区分不同父类型的子代,那么整个调解过程就会大幅减慢。
所以你只剩下你提出的带有 props 的通用父级解决方案了。
答案 2 :(得分:2)
这不是一个技术性的答案,但是有 可以实现这一点的重新父类库:react-reverse-portal 和 react-reparenting。
我最近自己研究了它,但它们有点实验性,似乎需要担心很多开销。如果您实际上不需要跨多个分支移动组件,@deckele 的答案看起来是一个更好的解决方案。
答案 3 :(得分:1)
如果不了解更多有关此问题的背景以及您要实现的更大目标,就很难提供具体的解决方案。
首先,由于 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 树没有被改变!