← React/Списки и ключи#262 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

Списки и ключи

Рендеринг массивов с .map()

Для отображения списка данных в React используется стандартный метод массива .map(). Каждый элемент массива превращается в JSX-элемент:

function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name} — {user.email}
        </li>
      ))}
    </ul>
  )
}

Массив JSX-элементов React рендерит последовательно, добавляя каждый в DOM.

Зачем нужен key

React использует key для эффективного обновления DOM (reconciliation). Когда список меняется, React сравнивает старые и новые элементы по ключам:

  • Элемент с таким ключом существует → обновить если изменился
  • Элемент с таким ключом исчез → удалить из DOM
  • Появился новый ключ → создать новый DOM-узел
  • Без ключей 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 думает что изменились все элементы от этой позиции до конца, и перерисовывает их все.

    Индекс допустим только если список статичный (никогда не меняется) и не сортируется.

    Правила для key

  • Должен быть уникальным среди соседей (не глобально)
  • Должен быть стабильным (не меняться при перерендере)
  • Обычно это 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 — это не prop!

    Важно: 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-операциями.')

    Списки и ключи

    Рендеринг массивов с .map()

    Для отображения списка данных в React используется стандартный метод массива .map(). Каждый элемент массива превращается в JSX-элемент:

    function UserList({ users }) {
      return (
        <ul>
          {users.map(user => (
            <li key={user.id}>
              {user.name} — {user.email}
            </li>
          ))}
        </ul>
      )
    }

    Массив JSX-элементов React рендерит последовательно, добавляя каждый в DOM.

    Зачем нужен key

    React использует key для эффективного обновления DOM (reconciliation). Когда список меняется, React сравнивает старые и новые элементы по ключам:

  • Элемент с таким ключом существует → обновить если изменился
  • Элемент с таким ключом исчез → удалить из DOM
  • Появился новый ключ → создать новый DOM-узел
  • Без ключей 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 думает что изменились все элементы от этой позиции до конца, и перерисовывает их все.

    Индекс допустим только если список статичный (никогда не меняется) и не сортируется.

    Правила для key

  • Должен быть уникальным среди соседей (не глобально)
  • Должен быть стабильным (не меняться при перерендере)
  • Обычно это 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 — это не prop!

    Важно: 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.

    Загружаем среду выполнения...
    Загружаем AI-помощника...