即使其`key`道具保持相同,React元素也会重置

时间:2019-05-10 17:49:13

标签: javascript reactjs

我正在使用React创建一个Web应用程序,并且遇到了这个奇怪的问题。

作为总结,当添加或删除一个同级兄弟时,用花括号(例如:{[<Element />, <Element />]}表示为数组的子元素会重置。

我的问题是React是否期望这种行为?如果是,为什么会发生?

为了说明,我想出了两个例子。它们的代码完全相同,除了第一个在JSX中直接声明元素,第二个在数组内声明它们(可以由Array.map生成):

Ticker是组成用来演示状态的通用组件。 DummyElement是没有状态的通用组件。 App是根组件。

在第一个示例中,您可以看到,在布局之间进行切换时,即在添加或删除DummyElement时,将保留Tickers状态。考虑到Tickers key道具保持不变,这是我期望的行为。

但是,在第二个示例中,每当在布局之间切换时,Ticker状态都会重置。这进一步显示在控制台中,该控制台记录每次更改布局时都在挂载和卸载Tickers

编辑:

我提出了一个与问题有关的issue:)

2 个答案:

答案 0 :(得分:3)

react渲染多个children时,会将其视为子元素的数组,但是当children单个时子,然后react会将其视为单个元素。
在您的情况下,很有趣的是,在第一个条件下,children的{​​{1}}是一个<div className="top">,但实际上是一个子“元素”:

array

如果将其视为react元素,我们将大致看到以下内容:

<div className="top">
  {[<Ticker name="1" />, <Ticker name="2" />]}
</div>

但是在第二种情况下,我们有2个孩子:

{
  type: 'div',
  className: 'top',
  children: [<Ticker name="1" />, <Ticker name="2" />]
}

因此,基本上我们有一个<div className="top"> {[<Ticker name="1" />, <Ticker name="2" />]} <DummyElement key="3" /> </div> 个子级,其中包含另一个元素数组 AND 。另一个元素。
如果将其视为react元素,我们将看到大致如下所示的内容:

array

因此,在两种情况下,{ type: 'div', className: 'top', children: [ [<Ticker name="1" />, <Ticker name="2" />], <DummyElement key="3" /> ] } 子级都是一个数组(碰巧),但是数组成员的类型正在更改:
在第一种情况下,type的第一个成员是array元素。
在第二种情况下,Ticker的第一个成员是另一个array

因此,当react在执行其reconciliation进程时,它将检查以下内容:

  
      
  1. 两个不同类型的元素将产生不同的树。
  2.   
  3. 开发人员可以使用关键道具提示哪些子元素在不同的渲染中可能稳定。
  4.   

因此,您的情况属于第一张支票:

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"/>来包装),则可以用React.Fragment来包装它们,只需确保提供相同的{{ 1}}。请注意,没有div的{​​{1}}被视为一个数组,key将始终“认为”它是一个新的实例主机,因此将重新创建它(及其子元素)。

这是您第二个示例的示例,但具有预期的行为:

Fragment
key
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> 将其展平为单个元素)。 br /> 原来,当我们使用children运算符时,react将使用右侧(当条件为react时)或&&(当条件为{{1}时) })和react将在true中保留一个“空洞”。表示我们将始终获得null中的false

因此,我们最终得到以下元素:

null

这是一个正在运行的示例:

array
array
children

答案 1 :(得分:2)

之所以会看到这种情况,是因为您更改了示例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>
    );
  }

https://jsfiddle.net/L1syr347/1/