На вопрос ответил автор вопроса, я в свою очередь попытаюсь объяснить что произошло.
Рендер в реакте всегда идёт по наименее трудозатратному пути, а именно вычисляет дельту (разницу) между новым стейтом и предыдущим, если таковой имеется. Например:
class TodoList extends React.Component {
state = {
todos: [
'Commit',
'Push'
]
}
render() {
return <ul>
{this.state.todos.map(item => {
return <li>{ todo }</li>
}
</ul>
}
}
Если стейт компонента изменится, скажем при добавление элемента в начало списка todos, так что он станет таким:
const todos = [
'Init',
'Commit',
'Push'
]
Реакт вычислит два древа VirtualDOM:
// Начальный стейт
<ul>
<li>Commit</li>
<li>Push</li>
</ul>
// Добавлен элемент
<ul>
<li>Init</li> // <- разница начинается здесь и до конца древа
<li>Commit</li>
<li>Push</li>
</ul>
Здесь выполняется работа которой можно было бы избежать. К примеру если бы элемент был добавлен в конец списка:
const todos = [
'Commit',
'Push',
'Merge'
]
То реакт получил бы на сравнение другие два древа элементов:
// Начальный стейт
<ul>
<li>Commit</li>
<li>Push</li>
</ul>
// Добавлен элемент
<ul>
<li>Commit</li>
<li>Push</li>
<li>Merge</li> <- разница начинается здесь, от начала и до сих по ничего не менялось
</ul>
Дельта этих двух списков меньше, а значит и работы нужно сделать меньше.
Совершенно очевидно что
<li>Commit</li>
и
<li>Push</li>
не менялись, однако реакт недостаточно умён чтобы это понять. Чтобы помочь ему следует воспользоваться специальным пропом
key={}
. Он может быть значением любого типа, единственно требование — значение должно стабильно идентифицировать соответствующие данные.
Если бы компонент выглядел так:
class TodoList extends React.Component {
state = {
todos: [
{ id: 0, text: 'Commit' },
{ id: 1, text: 'Push' }
]
}
render() {
return <ul>
{this.state.todos.map(item => {
return <li key={todo.id}>{ todo.text }</li>
}
</ul>
}
}
То добавление элемента в начало массива, породило бы следующий стейт:
const todos = [
{ id: 2, text: 'Init' },
{ id: 0, text: 'Commit' },
{ id: 1, text: 'Push' }
]
И, снова, два древа элементов:
// Начальный стейт
<ul>
<li>Commit</li> // id 0
<li>Push</li> // id 1
</ul>
// Добавлен элемент
<ul>
<li>Init</li> // id 2 новый элемент отобразится в начале
<li>Commit</li> // id 0
<li>Push</li> // id 1
</ul>
Благодаря подсказке, реакт не учтёт в дельте элементы чьи идентификаторы не изменились, и, как следствие, не будет делать лишних действий.
Таким образом использовать индекс в массиве в качестве ключа — не лучшая идея, особенно если массив будет меняться. По той же причине не следует использовать
Math.random()
в качестве ключа, так вы почти гарантировано будете всегда получать нестабильные идентификаторы.
Подробнее об этом можно почитать здесь —
Reconciliation.