首页 文章

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

提问于
浏览
368

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

数组中的每个子节点都应该具有唯一的“键”支柱 . 检查TableComponent的render方法 .

我的 TableComponent render方法返回:

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

TableHeader 组件是单行,并且还为其分配了唯一键 .

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

<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>
    )
  }
});

导致唯一关键道具错误的原因是什么?

7 回答

  • 280

    您应该为每个孩子添加一个键 as well as each element inside children .

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

    在你的代码中,每个 <TableRowItem key={item.id} data={item} columns={columnNames}/> 都试图在没有密钥的情况下渲染其中的一些孩子 .

    检查this example .

    尝试从div中的 <b></b> 元素中删除 key={i} (并检查控制台) .

    在示例中,如果我们没有为 <b> 元素提供密钥,并且我们想要更新 only object.city ,则React需要重新渲染整行而不是元素 .

    这是代码:

    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);
    

    The answer posted by @Chris at the bottom goes into much more detail than this answer. Please take a look at https://stackoverflow.com/a/43892905/2325522

    关于对帐中密钥重要性的文档:Keys

  • 371

    Be careful when iterating over arrays!!

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

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

    但是,在许多情况下它不是!这是 anti-pattern ,在某些情况下可以导致 unwanted behavior .

    了解关键道具

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

    话虽如此,如果已知阵列是完全静态的,则并不总是需要应用上述内容 . 但是,尽可能鼓励应用最佳实践 .

    React开发人员在this GitHub issue中说:

    关键不是关于性能,而是关于身份(这反过来导致更好的性能) . 随机分配和更改的值不是标识我们无法在不知道您的数据建模方式的情况下[自动]实际提供密钥 . 如果你没有id,我建议使用某种散列函数我们在使用数组时已经有了内部键,但它们是数组中的索引 . 插入新元素时,这些键是错误的 .

    简而言之, key 应该是:

    • Unique - 密钥不能与sibling component的密钥相同 .

    • Static - 渲染之间的密钥不应该改变 .

    使用关键道具

    根据上述说明,仔细研究以下样本,并尽可能尝试实施推荐的方法 .


    坏(可能)

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

    这可以说是在React中迭代数组时最常见的错误 . 这种方法在技术上并不是"wrong",如果您不知道自己在做什么,那只是...... "dangerous" . 如果您正在迭代静态数组,那么这是一种非常有效的方法(例如导航菜单中的链接数组) . 但是,如果要添加,删除,重新排序或过滤项目,则需要小心 . 在官方文档中查看detailed explanation .

    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 1key={0} . 当我们添加第二个项目时,顶部项目变为 Item 2 ,然后是 Item 1 作为第二个项目 . 但是,现在 Item 1 已经 key={1} 而不是 key={0} 了 . 相反, Item 2 现在有 key={0} !!

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

    So why is this approach only sometimes bad?

    如果以某种方式过滤,重新排列或添加/删除项目,则此方法仅有风险 . 如果它总是静止的,那么使用起来非常安全 . 例如,使用此方法可以安全地迭代像 ["Home", "Products", "Contact us"] 这样的导航菜单,因为您可能永远不会添加新链接或重新排列它们 .

    简而言之,这时你可以 safely 将索引用作 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>
    

    这可以说是best approach因为它使用了对数据集中每个项目唯一的属性 . 例如,如果 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 .

  • 0

    警告:数组或迭代器中的每个子项都应该具有唯一的“键”支柱 .

    这是一个警告,因为我们要迭代的数组项需要一个独特的相似性 .

    React处理迭代组件呈现为数组 .

    更好的解决方法是为要迭代的数组项提供索引 . 例如:

    class UsersState extends Component
        {
            state = {
                users: [
                    {name:"shashank", age:20},
                    {name:"vardan", age:30},
                    {name:"somya", age:40}
                ]
            }
        render()
            {
                return(
                        <div>
                            {
                                this.state.users.map((user, index)=>{
                                    return <UserState key={index} age={user.age}>{user.name}</UserState>
                                })
                            }
                        </div>
                    )
            }
    

    index是React内置道具 .

  • 1

    只需将 unique key 添加到您的组件即可

    data.map((marker)=>{
        return(
            <YourComponents 
                key={data.id}     // <----- unique key
            />
        );
    })
    
  • 1

    我使用Guid为每个键修复了这个问题:生成Guid:

    guid() {
        return this.s4() + this.s4() + '-' + this.s4() + '-' + this.s4() + '-' +
            this.s4() + '-' + this.s4() + this.s4() + this.s4();
    }
    
    s4() {
        return Math.floor((1 + Math.random()) * 0x10000)
            .toString(16)
            .substring(1);
    }
    

    然后将此值分配给标记:

    {this.state.markers.map(marker => (
                  <MapView.Marker
                      key={this.guid()}
                      coordinate={marker.coordinates}
                      title={marker.title}
                  />
              ))}
    
  • 1

    示例:更正密钥用法

    function ListItem(props) {
      // Correct! There is no need to specify the key here:
      return <li>{props.value}</li>;
    }
    
    function NumberList(props) {
      const numbers = props.numbers;
      const listItems = numbers.map((number) =>
        // Correct! Key should be specified inside the array.
        <ListItem key={number.toString()}
                  value={number} />
    
      );
      return (
        <ul>
          {listItems}
        </ul>
      );
    }
    
    const numbers = [1, 2, 3, 4, 5];
    ReactDOM.render(
      <NumberList numbers={numbers} />,
      document.getElementById('root')
    );
    
  • 1

    这是一个警告,但 addressing this will make Reacts rendering much FASTER

    这是因为 React 需要唯一标识列表中的每个项目 . 假设如果该列表的元素状态在Reacts Virtual DOM 中发生变化,则React需要确定哪个元素已更改以及DOM中需要更改的位置,以便浏览器DOM与Reacts Virtual DOM同步 .

    作为解决方案,只需为每个 li 标记引入 key 属性 . key 应该是每个元素的唯一值 .

相关问题