[英]React element resets even when its `key` prop is kept the same
我正在使用React创建一个Web应用程序,并遇到了这个奇怪的问题。
总而言之,在添加或删除一个兄弟时,将重置表示为花括号内的数组的子元素(例如: {[<Element />, <Element />]}
。
我的问题是React是否期望这种行为,如果是,为什么会发生?
为了说明,我提出了两个例子。 它们的代码完全相同,只是第一个在JSX中直接声明元素,第二个在数组中声明它们(可以由Array.map
生成):
Ticker
是一个用于演示状态的通用组件。 DummyElement
是一个没有任何状态的通用组件。 App
是根组件。
在第一个例子中,可以看到的是,布局之间进行切换时,即,添加或移除时DummyElement
,所述Tickers
状态被保留。 考虑到Tickers
key
道具保持不变,这是我期望的行为。
但是,在第二个示例中,只要在布局之间切换,就会重置Ticker
状态。 这在控制台中进一步显示,记录在每次布局更改时安装和卸载Tickers
。
编辑:
我提出了一个与问题有关的问题:)
当react
呈现多个children
,它将其视为一系列儿童,但当children
是单个孩子时,则react
会将其视为单个元素。
你的情况很有趣地看到,在第一个条件的children
中的<div className="top">
是一个array
,但实际上是一个孩子“元素”:
<div className="top">
{[<Ticker name="1" />, <Ticker name="2" />]}
</div>
如果我们将其视为反应元素,我们将看到类似于此的东西:
{
type: 'div',
className: 'top',
children: [<Ticker name="1" />, <Ticker name="2" />]
}
但在第二个条件下,我们有2个孩子:
<div className="top">
{[<Ticker name="1" />, <Ticker name="2" />]}
<DummyElement key="3" />
</div>
所以基本上我们有一个子array
,它包含另一个元素数组和另一个元素。
如果我们将其视为反应元素,我们将看到类似于此的东西:
{
type: 'div',
className: 'top',
children: [
[<Ticker name="1" />, <Ticker name="2" />],
<DummyElement key="3" />
]
}
所以在这两种情况下,子type
都是一个数组(巧合),但数组成员的类型正在改变:
在第一种情况下, array
的第一个成员是Ticker
元素。
在第二种情况下, array
的第一个成员是另一个array
因此,当反应正在进行其对帐过程时,它将检查以下内容:
- 不同类型的两个元素将产生不同的树木。
- 开发人员可以通过关键道具暗示哪些子元素可以在不同渲染中保持稳定。
所以你的情况是第一次检查:
type Ticker -> type Array
为了证明这一点,我创建了与你相同的例子,但我添加了一个额外的元素作为子元素,因此children
元素将始终是一种类型的array
,这样我们将始终获得如下元素:
{
type: 'div',
className: 'top',
children: [
{type: 'div'},
[<Ticker name="1" />, <Ticker name="2" />],
/* DummyElement will be added conditionally */
]
}
这是一个运行的例子(注意我保留了孩子们的位置):
class App extends React.Component { constructor(props) { super(props); this.state = { layout: 1 }; } render() { let toRender = null; if (this.state.layout == 1) toRender = this._renderLayout1(); else if (this.state.layout == 2) toRender = this._renderLayout2(); return toRender; } _renderLayout1() { return ( <div> <div className="top"> <div>I'm forcing children as array</div> {[<Ticker name="1" />, <Ticker name="2" />]} </div> <div className="bottom">{this._renderButtons()}</div> </div> ); } _renderLayout2() { return ( <div> <div className="top"> <div>I'm forcing children as array</div> {[<Ticker name="1" />, <Ticker name="2" />]} <DummyElement key="3" /> </div> <div className="bottom">{this._renderButtons()}</div> </div> ); } _renderButtons() { return ( <React.Fragment> <button onClick={() => this.setState({ layout: 1 })}>2x Ticker</button> <button onClick={() => this.setState({ layout: 2 })}> 2x Ticker + DummyElement </button> </React.Fragment> ); } } class Ticker extends React.Component { // Display seconds from the moment I'm created. constructor(props) { super(props); this.state = { tickNumber: 0 }; } componentDidMount() { console.log(`Mount Ticker "${this.props.name}"`); this.timerID = setInterval(() => { this.setState(prevState => ({ tickNumber: prevState.tickNumber + 1 })); }, 1000); } componentWillUnmount() { console.log(`Unmount Ticker "${this.props.name}"`); clearInterval(this.timerID); } render() { const displayTick = String(this.state.tickNumber).padStart(4, 0); const displayStr = `Ticker "${this.props.name}" - ${displayTick}`; return <div className="Ticker">{displayStr}</div>; } } function DummyElement() { return <div className="DummyElement">Dummy element</div>; } ReactDOM.render(<App />, document.querySelector("#root"));
.top, .bottom { margin: 1em; } .Ticker, .DummyElement { display: inline-block; margin-right: 1em; border: 1px solid black; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <div id="root"/>
遗憾的是,我们无法为数组提供键,因此在您的情况下,它将始终为这些数组重新创建树,但我们可以使用元素包装它们。
如果你不能用一个额外的元素包装数组(比如你的第一个带有包装div
例子),你可以使用React.Fragment包装它们,只需确保提供相同的key
。 请注意,没有key
的Fragment
被视为数组, react
将始终“认为”它是一个新的实例主机,因此将重新创建它(及其子节点)。
以下是您的第二个示例的示例,但具有所需的行为:
class App extends React.Component { constructor(props) { super(props); this.state = {layout : 1}; } render() { if (this.state.layout == 1) return this._renderLayout1(); else if (this.state.layout == 2) return this._renderLayout2(); } _renderLayout1() { return ( <div> <div className="top"> <React.Fragment key="1"> {[ <Ticker key="1" name="1" />, <Ticker key="2" name="2" /> ]} </React.Fragment> </div> <div className="bottom"> {this._renderButtons()} </div> </div> ); } _renderLayout2() { return ( <div> <div className="top"> <React.Fragment key="1"> {[ <Ticker key="1" name="1" />, <Ticker key="2" name="2" /> ]} </React.Fragment> <DummyElement key="3" /> </div> <div className="bottom"> {this._renderButtons()} </div> </div> ); } _renderButtons() { return ( <React.Fragment> <button onClick={ () => this.setState({'layout': 1}) }> 2x Ticker </button> <button onClick={ () => this.setState({'layout': 2}) }> 2x Ticker + DummyElement </button> </React.Fragment> ); } } class Ticker extends React.Component { // Display seconds from the moment I'm created. constructor(props) { super(props); this.state = {tickNumber: 0}; } componentDidMount() { console.log(`Mount Ticker "${this.props.name}"`); this.timerID = setInterval( () => { this.setState( prevState => ({tickNumber: prevState.tickNumber + 1}) ); }, 1000 ); } componentWillUnmount() { console.log(`Unmount Ticker "${this.props.name}"`); clearInterval(this.timerID); } render() { const displayTick = String(this.state.tickNumber).padStart(4, 0); const displayStr = `Ticker "${this.props.name}" - ${displayTick}`; return ( <div className="Ticker"> {displayStr} </div> ); } } function DummyElement() { return ( <div className="DummyElement"> Dummy element </div> ); } ReactDOM.render(<App />, document.querySelector("#root"))
.top, .bottom { margin: 1em; } .Ticker, .DummyElement { display: inline-block; margin-right: 1em; border: 1px solid black; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <div id="root"></div>
话虽如此,我认为这里更好,更易读的方法是按原样呈现所有内容,并且只有条件地呈现DummyElement
。
<div className="top">
{[<Ticker key="1" name="1" />, <Ticker key="2" name="2" />]}
{layout === 2 && <DummyElement key="3" />}
</div>
但为什么这个工作符合预期呢? 我的意思是在这种情况下将再次提供children
元素作为多个元素( react
将转换为数组)或单个元素(其react
会将其展平为单个元素)。
事实证明,当我们使用&&
运算符时, react
将使用右侧(当条件为true
)或null
(当条件为false
)并且null
将在array
保留“空洞”。 这意味着我们将永远得到array
的children
。
因此我们最终得到这个元素:
{
type: 'div',
className: 'top',
children: [
[<Ticker name="1" />, <Ticker name="2" />],
null || DummyElement
]
}
这是一个运行的例子:
class App extends React.Component { constructor(props) { super(props); this.state = { layout: 1 }; } render() { return this._renderLayout(); } _renderLayout() { const { layout } = this.state; return ( <div> <div className="top"> {[<Ticker key="1" name="1" />, <Ticker key="2" name="2" />]} {layout === 2 && <DummyElement key="3" />} </div> <div className="bottom">{this._renderButtons()}</div> </div> ); } _renderButtons() { return ( <React.Fragment> <button onClick={() => this.setState({ layout: 1 })}>2x Ticker</button> <button onClick={() => this.setState({ layout: 2 })}> 2x Ticker + DummyElement </button> </React.Fragment> ); } } class Ticker extends React.Component { // Display seconds from the moment I'm created. constructor(props) { super(props); this.state = { tickNumber: 0 }; } componentDidMount() { console.log(`Mount Ticker "${this.props.name}"`); this.timerID = setInterval(() => { this.setState(prevState => ({ tickNumber: prevState.tickNumber + 1 })); }, 1000); } componentWillUnmount() { console.log(`Unmount Ticker "${this.props.name}"`); clearInterval(this.timerID); } render() { const displayTick = String(this.state.tickNumber).padStart(4, 0); const displayStr = `Ticker "${this.props.name}" - ${displayTick}`; return <div className="Ticker">{displayStr}</div>; } } function DummyElement() { return <div className="DummyElement">Dummy element</div>; } ReactDOM.render(<App />, document.querySelector("#root"));
.top, .bottom { margin: 1em; } .Ticker, .DummyElement { display: inline-block; margin-right: 1em; border: 1px solid black; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <div id="root"/>
您看到这个是因为您在示例2中更改了树的拓扑结构(标记和数组的嵌套方式):
这是一个没有重置状态的修改版本,我在拓扑中保留了数组和非数组节点:
_renderLayout1() {
return (
<div>
<div className="top">
<span>
{[
<Ticker key="1" name="1" />,
<Ticker key="2" name="2" />
]}
</span>
</div>
<div className="bottom">
{this._renderButtons()}
</div>
</div>
);
}
_renderLayout2() {
return (
<div>
<div className="top">
<span>
{[
<Ticker key="1" name="1" />,
<Ticker key="2" name="2" />
]}
</span>
<DummyElement/>
</div>
<div className="bottom">
{this._renderButtons()}
</div>
</div>
);
}
https://jsfiddle.net/L1syr347/
这是另一个保留拓扑的版本,我已将所有内容放入数组中:
_renderLayout1() {
return (
<div>
<div className="top">
{[
<Ticker key="1" name="1" />,
<Ticker key="2" name="2" />
]}
</div>
<div className="bottom">
{this._renderButtons()}
</div>
</div>
);
}
_renderLayout2() {
return (
<div>
<div className="top">
{[
<Ticker key="1" name="1" />,
<Ticker key="2" name="2" />,
<DummyElement key="3"/>
]}
</div>
<div className="bottom">
{this._renderButtons()}
</div>
</div>
);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.