协慌网

登录 贡献 社区

了解 React.js 中数组子项的唯一键

我正在构建一个 React 组件,该组件接受 JSON 数据源并创建一个可排序的表。
每个动态数据行都有一个分配给它的唯一键,但是我仍然遇到以下错误:

数组中的每个子代都应具有唯一的 “键” 道具。
检查 TableComponent 的渲染方法。

我的TableComponent渲染方法返回:

<table>
  <thead key="thead">
    <TableHeader columns={columnNames}/>
  </thead>
  <tbody key="tbody">
    { rows }
  </tbody>
</table>

TableHeader组件是单行,还为它分配了唯一的键。

row中的每一rows都是由具有唯一键的组件构建的:

<TableRowItem key={item.id} data={item} columns={columnNames}/>

并且TableRowItem看起来像这样:

var TableRowItem = React.createClass({
  render: function() {

    var td = function() {
        return this.props.columns.map(function(c) {
          return <td key={this.props.data[c]}>{this.props.data[c]}</td>;
        }, this);
      }.bind(this);

    return (
      <tr>{ td(this.props.item) }</tr>
    )
  }
});

是什么导致唯一的按键道具错误?

答案

您应该为每个子项以及子项中的每个元素添加一个键。

这样,React 可以处理最小的 DOM 更改。

在您的代码中,每个<TableRowItem key={item.id} data={item} columns={columnNames}/>都试图在其中创建一些没有键的子级。

检查此示例

尝试从<b></b> key={i} (并检查控制台)。

在示例中,如果我们不给钥匙<b>元素,我们希望更新object.city ,做出反应需要重新渲染整个行 VS 只是元素。

这是代码:

var data = [{name:'Jhon', age:28, city:'HO'},
            {name:'Onhj', age:82, city:'HN'},
            {name:'Nohj', age:41, city:'IT'}
           ];

var Hello = React.createClass({

    render: function() {

      var _data = this.props.info;
      console.log(_data);
      return(
        <div>
            {_data.map(function(object, i){
               return <div className={"row"} key={i}> 
                          {[ object.name ,
                             // remove the key
                             <b className="fosfo" key={i}> {object.city} </b> , 
                             object.age
                          ]}
                      </div>; 
             })}
        </div>
       );
    }
});

React.render(<Hello info={data} />, document.body);

@Chris在底部发布的答案比该答案要详细得多。请看一下https://stackoverflow.com/a/43892905/2325522

对文档进行核对,了解密钥在对帐中的重要性:密钥

遍历数组时要小心!!

一个常见的误解是,使用数组中元素的索引是抑制您可能熟悉的错误的可接受方法:

Each child in an array should have a unique "key" prop.

但是,在许多情况下却并非如此!这是一种反模式,在某些情况下可能导致不良行为


了解key道具

React 使用key道具来了解组件与 DOM 元素的关系,然后将其用于对帐过程。因此,密钥始终保持唯一性非常重要,否则 React 很有可能会混淆元素并变异不正确的元素。同样重要的是,这些键在所有重新渲染过程中都应保持静态,以保持最佳性能。

就是说,只要知道阵列是完全静态的,就不必总是应用上述方法。但是,在可能的情况下,鼓励采用最佳实践。

一个 React 开发人员在这个 GitHub 问题中说

  • 关键不在于性能,而在于身份(反过来又可以带来更好的性能)。随机分配且变化的值不是身份
  • 在不知道数据建模方式的情况下,我们无法现实地 [自动] 提供密钥。如果您没有 ID,我建议您使用某种哈希函数
  • 使用数组时,我们已经具有内部键,但是它们是数组中的索引。当您插入新元素时,这些键是错误的。

简而言之, key应该是:

  • 唯一- 密钥不能与兄弟组件的密钥相同。
  • 静态- 关键不应在渲染之间更改。

使用key道具

根据上面的说明,请仔细研究以下示例,并在可能的情况下尝试实施推荐的方法。


不好(可能)

<tbody>
    {rows.map((row, i) => {
        return <ObjectRow key={i} />;
    })}
</tbody>

可以说这是在 React 中遍历数组时最常见的错误。从技术上讲,这种方法不是“错误的” ,只是…… 如果您不知道自己在做什么,则是 “危险的”。如果要遍历静态数组,则这是一种完全有效的方法(例如,导航菜单中的链接数组)。但是,如果要添加,删除,重新排序或过滤项目,则需要小心。请看一下官方文档中的详细说明。

class MyApp extends React.Component {
  constructor() {
    super();
    this.state = {
      arr: ["Item 1"]
    }
  }
  
  click = () => {
    this.setState({
      arr: ['Item ' + (this.state.arr.length+1)].concat(this.state.arr),
    });
  }
  
  render() {
    return(
      <div>
        <button onClick={this.click}>Add</button>
        <ul>
          {this.state.arr.map(
            (item, i) => <Item key={i} text={"Item " + i}>{item + " "}</Item>
          )}
        </ul>
      </div>
    );
  }
}

const Item = (props) => {
  return (
    <li>
      <label>{props.children}</label>
      <input value={props.text} />
    </li>
  );
}

ReactDOM.render(<MyApp />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>

在此代码段中,我们使用的是非静态数组,我们并不仅限于将其用作堆栈。这是一种不安全的方法(您会明白为什么)。请注意,当我们将项目添加到数组的开头(基本上未移位)时,每个<input>的值都保持不变。为什么?因为key不能唯一地标识每个项目。

换句话说,第一Item 1最初具有key={0} 。当我们添加第二个项目时,最上面的项目变成Item 2 ,然后是Item 1作为第二个项目。但是,现在Item 1具有key={1}而不是key={0} 。而是, Item 2现在具有key={0}

因此,React 认为<input>元素没有改变,因为键0 Item始终位于顶部!

那么,为什么这种方法有时只是不好的呢?

仅当以某种方式对数组进行过滤,重新排列或添加 / 删除项目时,此方法才有风险。如果它始终是静态的,那么使用它是绝对安全的。 ["Home", "Products", "Contact us"]类的导航菜单,因为您可能永远不会添加新链接或重新排列它们。

简而言之,这是您可以安全地将索引用作key

  • 该数组是静态的,永远不会改变。
  • 永远不会过滤数组(显示数组的子集)。
  • 阵列从不重新排序。
  • 该阵列用作堆栈或 LIFO(后进先出)。换句话说,添加只能在数组的末尾进行(即推入),而只有最后一项可以被删除(即弹出)。

相反,如果我们在上面的代码段中将添加的项目推到数组的末尾,则每个现有项目的顺序将始终是正确的。


很坏

<tbody>
    {rows.map((row) => {
        return <ObjectRow key={Math.random()} />;
    })}
</tbody>

尽管此方法可能会保证键的唯一性,但即使不需要时,它也会始终做出反应以重新呈现列表中的每个项目。这是一个非常糟糕的解决方案,因为它会极大地影响性能。 Math.random()两次产生相同的数字,则不能排除发生键冲突的可能性。

不稳定的键(如Math.random()产生的键)将导致不必要地重新创建许多组件实例和 DOM 节点,这可能导致性能下降和子组件中的状态丢失。


很好

<tbody>
    {rows.map((row) => {
        return <ObjectRow key={row.uniqueId} />;
    })}
</tbody>

可以说这是最好的方法,因为它使用的属性对于数据集中的每个项目都是唯一的。例如,如果rows包含从数据库中获取的数据,则可以使用表的主键(通常是自动递增的数字)。

挑选键的最佳方法是使用一个字符串,该字符串唯一地标识其同级项中的列表项。大多数情况下,您会将数据中的 ID 用作密钥


好的

componentWillMount() {
  let rows = this.props.rows.map(item => { 
    return {uid: SomeLibrary.generateUniqueID(), value: item};
  });
}

...

<tbody>
    {rows.map((row) => {
        return <ObjectRow key={row.uid} />;
    })}
</tbody>

这也是一个好方法。如果您的数据集不包含任何保证唯一性的数据(例如,任意数字的数组),则可能会发生键冲突。在这种情况下,最好在迭代之前为数据集中的每个项目手动生成唯一的标识符。最好在安装组件时或在接收到数据集时(例如,从props或从异步 API 调用接收到),以便仅执行一次,而不是每次组件都重新渲染。已经有少数可以为您提供此类密钥的库。这是一个示例: react-key-index

这可能对某人没有帮助,但可能是快速参考。这也与上面给出的所有答案相似。

我有很多使用以下结构生成列表的位置:

return (
    {myList.map(item => (
       <>
          <div class="some class"> 
             {item.someProperty} 
              ....
          </div>
       </>
     )}
 )

经过一番尝试和错误(和一些挫折)后,将键属性添加到最外层的块即可解决该问题。另外,请注意, <>标记替换为<div>标记。

return (
  
    {myList.map((item, index) => (
       <div key={index}>
          <div class="some class"> 
             {item.someProperty} 
              ....
          </div>
       </div>
     )}
 )

当然,在上面的示例中,我一直很天真地使用迭代索引(index)来填充键值。理想情况下,您将使用列表项特有的内容。