Понимание уникальных ключей для дочерних массивов в React.js

404

Я создаю компонент 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>
    )
  }
});

Что вызывает уникальную ошибку подсказки ключа?

  • 6
    Ваши строки в массиве JS должны иметь уникальное свойство key . Это поможет ReactJS найти ссылки на соответствующие узлы DOM и обновить только содержимое внутри разметки, но не перерисовывать всю таблицу / строку.
  • 0
    Вы также можете поделиться массивом rows или, более предпочтительно, jsfiddle? Кстати, вам не нужно key свойство для thead и tbody .
Показать ещё 2 комментария
Теги:

8 ответов

364
Лучший ответ

Вы должны добавить ключ к каждому дочернему элементу, а также к каждому элементу внутри дочерних элементов.

Таким образом, React может справиться с минимальным изменением DOM.

В вашем коде каждый <TableRowItem key={item.id} data={item} columns={columnNames}/> пытается отобразить в них несколько дочерних <TableRowItem key={item.id} data={item} columns={columnNames}/> без ключа.

Проверьте этот пример.

Попробуйте удалить key={i} из элемента <b></b> внутри div (и проверьте консоль).

В этом примере, если мы не дадим ключ к элементу <b> и хотим обновить только 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);

Ответ, размещенный @Chris внизу, раскрывает гораздо больше деталей, чем этот ответ. Пожалуйста, посмотрите на https://stackoverflow.com/questions/28329382/understanding-unique-keys-for-array-children-in-react-js

Реагировать на документацию о важности ключей в сверке: ключи

  • 0
    Я добавил компонент, о котором вы говорите, к исходному вопросу.
  • 0
    Должен ли я добавить key к элементу <tr> ?
Показать ещё 14 комментариев
274

Будьте осторожны при повторении массивов!

Это распространенное заблуждение, что использование индекса элемента в массиве является приемлемым способом подавления ошибки, с которой вы, вероятно, знакомы:

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

Однако во многих случаях это не так! Это анти-шаблон, который в некоторых ситуациях может привести к нежелательному поведению.


Понимание key опоры

React использует key подсказку для понимания отношения компонента к DOM, которое затем используется для процесса согласования. Поэтому очень важно, чтобы ключ всегда оставался уникальным, иначе есть хорошая вероятность. Реакт будет смешивать элементы и мутировать неправильный. Также важно, чтобы эти ключи оставались статичными во всех повторных документах, чтобы поддерживать лучшую производительность.

При этом не всегда нужно применять вышеописанное, если известно, что массив полностью статичен. Однако, когда это возможно, рекомендуется применять передовой опыт.

Разработчик React сказал в этом выпуске GitHub:

  • ключ не совсем о производительности, это больше об идентичности (что, в свою очередь, ведет к лучшей производительности). случайно присвоенные и изменяющиеся значения не являются идентичными
  • Мы не можем реалистично предоставлять ключи [автоматически], не зная, как моделируются ваши данные. Я бы предложил, возможно, используя какую-то функцию хеширования, если у вас нет идентификаторов
  • У нас уже есть внутренние ключи, когда мы используем массивы, но они являются индексом в массиве. Когда вы вставляете новый элемент, эти ключи ошибочны.

Короче говоря, 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> не изменились, потому что Item с ключом 0 всегда наверху!

Так почему же этот подход иногда бывает плохим?

Этот подход является только рискованным, если массив каким-то образом фильтруется, переупорядочивается или элементы добавляются/удаляются. Если он всегда статичен, то он совершенно безопасен в использовании. Например, навигационное меню, например ["Home", "Products", "Contact us"] можно безопасно повторить с помощью этого метода, потому что вы, вероятно, никогда не добавите новые ссылки или не переупорядочите их.

Короче говоря, здесь, когда вы можете безопасно использовать индекс как key:

  • Массив статический и никогда не будет меняться.
  • Массив никогда не фильтруется (отображает подмножество массива).
  • Массив никогда не переупорядочивается.
  • Массив используется как стек или LIFO (последний раз, первый раз). Другими словами, добавление может выполняться только в конце массива (т.е. push), и только последний элемент может быть удален (т.е. поп).

Если бы мы вместо этого, в приведенном выше фрагменте кода, нажал добавленный элемент в конец массива, порядок для каждого существующего объекта всегда будет правильным.


Очень плохо

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

Хотя этот подход, вероятно, гарантирует уникальность ключей, он всегда будет реагировать на повторное отображение каждого элемента в списке, даже если это не требуется. Это очень плохое решение, так как оно сильно влияет на производительность. Не говоря уже о том, что нельзя исключать возможность столкновения с ключом в случае, если Math.random() производит одинаковый номер дважды.

Нестабильные ключи (например, созданные Math.random()) приведут к Math.random() многих экземпляров компонентов и узлов DOM, что может привести к ухудшению производительности и Math.random() в дочерних компонентах.


Отлично

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

Это, пожалуй, лучший подход, поскольку он использует свойство, уникальное для каждого элемента в наборе данных. Например, если rows содержат данные, извлеченные из базы данных, можно использовать основной ключ таблицы (обычно это число с автоматическим увеличением).

Лучший способ выбрать ключ - использовать строку, которая однозначно идентифицирует элемент списка среди своих братьев и сестер. Чаще всего вы будете использовать идентификаторы из ваших данных в качестве ключей


Хорошо

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), чтобы сделать это только один раз, а не каждый раз, когда компонент повторно отображает. Есть уже несколько библиотек, которые могут предоставить вам такие ключи. Вот один пример: index-key-index.

  • 1
    В официальных документах они используют toString() для преобразования в строку, а не в качестве числа. Это важно помнить?
  • 1
    @skube, нет, вы можете использовать целые числа в качестве key . Не уверен, почему они конвертируют это.
Показать ещё 11 комментариев
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

Я исправил это с помощью Guid для каждого ключа следующим образом: Generating 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}
              />
          ))}
  • 0
    Добавление случайного числа в качестве ключа вредно! Это предотвратит реакцию на обнаружение изменений, что является целью ключа.
1

Просто добавьте уникальный ключ к своим компонентам

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

Предупреждение: каждый дочерний элемент в массиве или итераторе должен иметь уникальную "ключевую" опору.

Это предупреждение, поскольку для элементов массива, которые мы собираемся перебирать, потребуется уникальное сходство.

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 - Реагировать встроенные реквизиты.

  • 2
    Такой подход потенциально опасен, если элементы каким-либо образом переставляются. Но если они остаются статичными, то это нормально.
  • 0
    @ Крис Я полностью согласен с вами, потому что в этом случае индекс может дублироваться. Лучше использовать динамические значения для ключа.
Показать ещё 1 комментарий
0

Самое простое решение, которое я нашел, - это создать уникальный идентификатор для каждого элемента в списке. Есть 2 библиотеки, uniqid и uuid.

npm install uniqid

Тогда в вашем коде:

import uniqueid from 'uniqid'
- OR if you use require:   var uniqid = require('uniqid');

Затем просто добавьте ключ к нужному элементу HTML. В приведенном ниже примере обратите внимание на ключ = {uniqueid()}

{this.state.someList.map((item) => <li>{item}</li>)}
I changed to this:
{this.state.someList.map((item) => <li key={uniqueid()}>{item}</li>)}
  • 0
    Я не думаю, что это хорошо, так как я считаю, что ключ в этом случае будет отличаться все время, что приведет к полному повторному рендерингу всех детей в каждом цикле рендеринга? Чтобы это работало, я думаю, что вам нужно извлечь генерацию идентификатора вне жизненного цикла рендера.
0

Это предупреждение, но решение этой проблемы сделает рендеринг намного быстрее,

Это потому, что React должен однозначно идентифицировать каждый элемент в списке. Допустим, если состояние элемента этого списка изменяется в Reacts Virtual DOM то React должен выяснить, какой элемент был изменен и где в DOM его нужно изменить, чтобы DOM браузера синхронизировался с Reacts Virtual DOM.

В качестве решения просто введите key атрибут для каждого тега li. Этот key должен иметь уникальное значение для каждого элемента.

  • 0
    Это не совсем правильно. Рендеринг не будет быстрее, если вы добавите key опору. Если вы его не предоставите, React назначит его автоматически (текущий индекс итерации).
  • 0
    @ Крис в таком случае, почему это вызывает предупреждение?
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню