Будьте обережні при ітерації над масивами !!
Поширена помилкова думка, що використання індексу елемента в масиві є прийнятним способом придушення помилки, з якою ви, мабуть, знайомі:
Each child in an array should have a unique "key" prop.
Однак у багатьох випадках це не так! Це анти-модель, яка в деяких ситуаціях може призвести до небажаної поведінки .
Розуміння key
опори
React використовує key
опору для розуміння відношення компонента до DOM, який потім використовується для процесу узгодження . Тому дуже важливо, щоб ключ завжди залишався унікальним , інакше є хороший шанс React змішати елементи та вимкнути неправильний. Також важливо, щоб ці ключі залишалися статичними протягом усіх відтворень, щоб підтримувати найкращу продуктивність.
Незважаючи на це, не завжди потрібно застосовувати вище, якщо відомо, що масив повністю статичний. Однак застосування кращих практик рекомендується, коли це можливо.
У цьому випуску GitHub розробник React сказав :
- Ключ насправді не в продуктивності, а більше в ідентичності (що в свою чергу призводить до кращої продуктивності). довільно присвоєні та змінюються значення не є тотожністю
- Ми не можемо реально надати ключі [автоматично], не знаючи, як моделюються ваші дані. Я б запропонував, можливо, використовувати якусь функцію хешування, якщо у вас немає ідентифікаторів
- У нас вже є внутрішні ключі, коли ми використовуємо масиви, але вони є індексом у масиві. Коли ви вставляєте новий елемент, ці клавіші неправильні.
Якщо коротко, a 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 (останній у, перший вихід). Іншими словами, додавання може бути здійснено лише в кінці масиву (тобто натискання), і тільки останній елемент можна буде коли-небудь видалити (тобто поп).
Якби ми, замість цього, у фрагменті вище натиснули доданий елемент до кінця масиву, порядок для кожного існуючого елемента завжди був би правильним.
Дуже погано
<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
містяться дані, отримані з бази даних, можна використовувати Первинний ключ таблиці ( який, як правило, є числом, що збільшується автоматично ).
Найкращий спосіб вибрати ключ - це використовувати рядок, який однозначно визначає елемент списку серед його побратимів. Найчастіше ви використовуєте ідентифікатори зі своїх даних як ключі
Добре
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 асинхронізації ), щоб зробити це лише один раз , а не кожен раз, коли компонент повторно відображається. Тут уже є кілька бібліотек, які можуть надати вам такі ключі. Ось один із прикладів: реакція-ключ-індекс .
key
властивість. Це допоможе ReactJS знаходити посилання на відповідні вузли DOM та оновлювати лише вміст всередині розмітки, але не відтворювати всю таблицю / рядок.