Для отображения списка данных в React используется стандартный метод массива .map(). Каждый элемент массива превращается в JSX-элемент:
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
)
}Массив JSX-элементов React рендерит последовательно, добавляя каждый в DOM.
React использует key для эффективного обновления DOM (reconciliation). Когда список меняется, React сравнивает старые и новые элементы по ключам:
Без ключей React вынужден перестраивать весь список при каждом изменении.
// НЕПРАВИЛЬНО — без ключей:
{items.map(item => <Item text={item.text} />)}
// Warning: Each child in a list should have a unique "key" prop
// ПРАВИЛЬНО — уникальный стабильный ключ:
{items.map(item => <Item key={item.id} text={item.text} />)}// ПЛОХО — ключ по индексу:
{items.map((item, index) => <Item key={index} text={item.text} />)}Если удалить элемент из середины списка — все индексы после него сдвигаются. React думает что изменились все элементы от этой позиции до конца, и перерисовывает их все.
Индекс допустим только если список статичный (никогда не меняется) и не сортируется.
id из базы данных или UUID// Хорошие ключи:
<Item key={user.id} /> // id из БД
<Item key={product.sku} /> // уникальный код
<Item key={message.timestamp} /> // уникальный timestamp
// Плохие ключи:
<Item key={index} /> // индекс — меняется
<Item key={Math.random()} /> // случайный — меняется каждый рендер!
<Item key={item.text} /> // текст — может повторятьсяfunction ProductList({ products, searchQuery, sortBy }) {
const filtered = products
.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price
if (sortBy === 'name') return a.name.localeCompare(b.name)
return 0
})
if (filtered.length === 0) {
return <p>Ничего не найдено</p>
}
return (
<ul>
{filtered.map(product => (
<li key={product.id}>{product.name} — {product.price} ₽</li>
))}
</ul>
)
}function CategoryList({ categories }) {
return (
<div>
{categories.map(category => (
<div key={category.id}>
<h2>{category.name}</h2>
<ul>
{category.products.map(product => (
<li key={product.id}>{product.name}</li>
// key уникален среди соседей — не нужно быть глобально уникальным
))}
</ul>
</div>
))}
</div>
)
}Важно: key — специальный атрибут React, он не доступен в компоненте через props.key. Если нужно передать id, передайте его явно:
// key нет в props внутри компонента:
function Item({ key, text }) { ... } // key всегда undefined!
// Правильно:
<Item key={item.id} id={item.id} text={item.text} />
function Item({ id, text }) { /* id доступен */ }Реализация виртуального DOM-диффинга с ключами — понимаем почему key так важен для производительности
// Реализуем упрощённый алгоритм reconciliation (сравнения списков)
// чтобы понять почему React требует уникальные стабильные ключи
// ============================================================
// Алгоритм diffing без ключей — O(n²) перестройки
// ============================================================
function diffWithoutKeys(oldList, newList) {
const operations = []
// Без ключей сравниваем только по позиции
const maxLen = Math.max(oldList.length, newList.length)
for (let i = 0; i < maxLen; i++) {
const oldItem = oldList[i]
const newItem = newList[i]
if (!oldItem && newItem) {
operations.push({ op: 'INSERT', item: newItem })
} else if (oldItem && !newItem) {
operations.push({ op: 'DELETE', item: oldItem })
} else if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) {
// Содержимое разное — UPDATE (даже если это просто сдвиг!)
operations.push({ op: 'UPDATE', old: oldItem, new: newItem })
}
}
return operations
}
// ============================================================
// Алгоритм diffing с ключами — O(n) оптимизированные обновления
// ============================================================
function diffWithKeys(oldList, newList, getKey) {
const operations = []
const oldMap = new Map(oldList.map(item => [getKey(item), item]))
const newMap = new Map(newList.map(item => [getKey(item), item]))
// Удалённые элементы: были в старом, нет в новом
for (const [key, item] of oldMap) {
if (!newMap.has(key)) {
operations.push({ op: 'DELETE', key, item })
}
}
// Новые элементы: нет в старом, есть в новом
for (const [key, item] of newMap) {
if (!oldMap.has(key)) {
operations.push({ op: 'INSERT', key, item })
} else if (JSON.stringify(oldMap.get(key)) !== JSON.stringify(item)) {
// Изменился только этот элемент
operations.push({ op: 'UPDATE', key, old: oldMap.get(key), new: item })
}
}
return operations
}
// ============================================================
// Демонстрация: удаление элемента из середины
// ============================================================
const oldList = [
{ id: 1, name: 'Алексей' },
{ id: 2, name: 'Мария' }, // <-- удаляем этот
{ id: 3, name: 'Дмитрий' },
]
const newList = [
{ id: 1, name: 'Алексей' },
// id: 2 удалён
{ id: 3, name: 'Дмитрий' },
]
console.log('=== Удаление элемента id=2 из середины ===')
console.log('\nБЕЗ ключей (по позиции):')
const opsWithout = diffWithoutKeys(oldList, newList)
opsWithout.forEach(op => console.log(' ', op.op, JSON.stringify(op.new || op.item)))
// UPDATE (id:2→id:3), DELETE (последний)
// React думает что изменились 2 элемента!
console.log(`Операций: ${opsWithout.length} (избыточно!)`)
console.log('\nС ключами (по id):')
const opsWith = diffWithKeys(oldList, newList, item => item.id)
opsWith.forEach(op => console.log(' ', op.op, 'key=' + op.key))
// Только DELETE key=2
// React точно знает: удалить только элемент с id=2
console.log(`Операций: ${opsWith.length} (оптимально!)`)
// Вывод:
console.log('\nВывод: key позволяет React делать O(n) diffing')
console.log('вместо O(n²) перестройки. Для списка в 1000 элементов')
console.log('это разница между 1 и 1000 DOM-операциями.')Для отображения списка данных в React используется стандартный метод массива .map(). Каждый элемент массива превращается в JSX-элемент:
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
)
}Массив JSX-элементов React рендерит последовательно, добавляя каждый в DOM.
React использует key для эффективного обновления DOM (reconciliation). Когда список меняется, React сравнивает старые и новые элементы по ключам:
Без ключей React вынужден перестраивать весь список при каждом изменении.
// НЕПРАВИЛЬНО — без ключей:
{items.map(item => <Item text={item.text} />)}
// Warning: Each child in a list should have a unique "key" prop
// ПРАВИЛЬНО — уникальный стабильный ключ:
{items.map(item => <Item key={item.id} text={item.text} />)}// ПЛОХО — ключ по индексу:
{items.map((item, index) => <Item key={index} text={item.text} />)}Если удалить элемент из середины списка — все индексы после него сдвигаются. React думает что изменились все элементы от этой позиции до конца, и перерисовывает их все.
Индекс допустим только если список статичный (никогда не меняется) и не сортируется.
id из базы данных или UUID// Хорошие ключи:
<Item key={user.id} /> // id из БД
<Item key={product.sku} /> // уникальный код
<Item key={message.timestamp} /> // уникальный timestamp
// Плохие ключи:
<Item key={index} /> // индекс — меняется
<Item key={Math.random()} /> // случайный — меняется каждый рендер!
<Item key={item.text} /> // текст — может повторятьсяfunction ProductList({ products, searchQuery, sortBy }) {
const filtered = products
.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price
if (sortBy === 'name') return a.name.localeCompare(b.name)
return 0
})
if (filtered.length === 0) {
return <p>Ничего не найдено</p>
}
return (
<ul>
{filtered.map(product => (
<li key={product.id}>{product.name} — {product.price} ₽</li>
))}
</ul>
)
}function CategoryList({ categories }) {
return (
<div>
{categories.map(category => (
<div key={category.id}>
<h2>{category.name}</h2>
<ul>
{category.products.map(product => (
<li key={product.id}>{product.name}</li>
// key уникален среди соседей — не нужно быть глобально уникальным
))}
</ul>
</div>
))}
</div>
)
}Важно: key — специальный атрибут React, он не доступен в компоненте через props.key. Если нужно передать id, передайте его явно:
// key нет в props внутри компонента:
function Item({ key, text }) { ... } // key всегда undefined!
// Правильно:
<Item key={item.id} id={item.id} text={item.text} />
function Item({ id, text }) { /* id доступен */ }Реализация виртуального DOM-диффинга с ключами — понимаем почему key так важен для производительности
// Реализуем упрощённый алгоритм reconciliation (сравнения списков)
// чтобы понять почему React требует уникальные стабильные ключи
// ============================================================
// Алгоритм diffing без ключей — O(n²) перестройки
// ============================================================
function diffWithoutKeys(oldList, newList) {
const operations = []
// Без ключей сравниваем только по позиции
const maxLen = Math.max(oldList.length, newList.length)
for (let i = 0; i < maxLen; i++) {
const oldItem = oldList[i]
const newItem = newList[i]
if (!oldItem && newItem) {
operations.push({ op: 'INSERT', item: newItem })
} else if (oldItem && !newItem) {
operations.push({ op: 'DELETE', item: oldItem })
} else if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) {
// Содержимое разное — UPDATE (даже если это просто сдвиг!)
operations.push({ op: 'UPDATE', old: oldItem, new: newItem })
}
}
return operations
}
// ============================================================
// Алгоритм diffing с ключами — O(n) оптимизированные обновления
// ============================================================
function diffWithKeys(oldList, newList, getKey) {
const operations = []
const oldMap = new Map(oldList.map(item => [getKey(item), item]))
const newMap = new Map(newList.map(item => [getKey(item), item]))
// Удалённые элементы: были в старом, нет в новом
for (const [key, item] of oldMap) {
if (!newMap.has(key)) {
operations.push({ op: 'DELETE', key, item })
}
}
// Новые элементы: нет в старом, есть в новом
for (const [key, item] of newMap) {
if (!oldMap.has(key)) {
operations.push({ op: 'INSERT', key, item })
} else if (JSON.stringify(oldMap.get(key)) !== JSON.stringify(item)) {
// Изменился только этот элемент
operations.push({ op: 'UPDATE', key, old: oldMap.get(key), new: item })
}
}
return operations
}
// ============================================================
// Демонстрация: удаление элемента из середины
// ============================================================
const oldList = [
{ id: 1, name: 'Алексей' },
{ id: 2, name: 'Мария' }, // <-- удаляем этот
{ id: 3, name: 'Дмитрий' },
]
const newList = [
{ id: 1, name: 'Алексей' },
// id: 2 удалён
{ id: 3, name: 'Дмитрий' },
]
console.log('=== Удаление элемента id=2 из середины ===')
console.log('\nБЕЗ ключей (по позиции):')
const opsWithout = diffWithoutKeys(oldList, newList)
opsWithout.forEach(op => console.log(' ', op.op, JSON.stringify(op.new || op.item)))
// UPDATE (id:2→id:3), DELETE (последний)
// React думает что изменились 2 элемента!
console.log(`Операций: ${opsWithout.length} (избыточно!)`)
console.log('\nС ключами (по id):')
const opsWith = diffWithKeys(oldList, newList, item => item.id)
opsWith.forEach(op => console.log(' ', op.op, 'key=' + op.key))
// Только DELETE key=2
// React точно знает: удалить только элемент с id=2
console.log(`Операций: ${opsWith.length} (оптимально!)`)
// Вывод:
console.log('\nВывод: key позволяет React делать O(n) diffing')
console.log('вместо O(n²) перестройки. Для списка в 1000 элементов')
console.log('это разница между 1 и 1000 DOM-операциями.')Создай компонент App, который хранит список фруктов в state и рендерит их через .map(). Каждый элемент списка должен иметь правильный key. Добавь кнопку "Удалить" рядом с каждым элементом, которая удаляет его из списка по id.
Используй .filter() для удаления: prev.filter(fruit => fruit.id !== id). В .map() каждый элемент должен иметь key={fruit.id}. onClick у кнопки: () => handleDelete(fruit.id). Доступ к полю: fruit.name.