Списки: @for, track и @empty

Блок @for рисует список из массива, а параметр track подсказывает Angular, как не перерисовывать то, что не изменилось.

«Без track Angular перерисует весь список при любом чихе. С track — точечно тронет лишь то, что реально поменялось».

Отображение списков — ежедневная задача: товары, сообщения, строки таблицы. Новый блок @for заменил директиву *ngFor и сделал обязательным то, что раньше забывали — track. Это выражение, которое уникально опознаёт каждый элемент, чтобы фреймворк отслеживал их между перерисовками.

@Component({
  selector: 'app-product-list',
  standalone: true,
  template: `
    <ul>
      @for (product of products; track product.id) {
        <li>{{ product.title }} — {{ product.price }} ₽</li>
      } @empty {
        <li>Список пуст</li>
      }
    </ul>
  `,
})
export class ProductListComponent {
  products = [
    { id: 1, title: 'Кофемолка', price: 2990 },
    { id: 2, title: 'Чайник', price: 1990 },
  ];
}

Блок @empty — приятный бонус: он рендерится, когда массив пуст, избавляя от отдельной проверки @if. Внутри цикла доступны контекстные переменные: $index (номер), $first, $last, $even, $odd.

template: `
  @for (item of items; track item.id; let i = $index) {
    <div [class.even]="$even">{{ i + 1 }}. {{ item.name }}</div>
  }
`

Как работает под капотом

Когда массив меняется, Angular берёт значение track у каждого элемента и сопоставляет старый список с новым. Совпал ключ — элемент DOM переиспользуется (обновляются только привязки). Не совпал — узел создаётся или удаляется. Без track по идентичности фреймворк не знал бы, какие узлы перерисовать, и пересоздавал бы весь список — это медленно и сбрасывает состояние (фокус, ввод).

   старый: [id:1] [id:2] [id:3]
   новый:  [id:2] [id:3] [id:4]
                |
        сопоставление по track id
                |
   id:1 удалён  ->  узел убран
   id:2,3 совпали -> узлы переиспользованы
   id:4 новый   ->  узел создан

Запускаемая врезка: diff списка по ключу track

Покажем, что делает track «руками». «Попробуй сам ▶».

// сравнение списков по ключу id — как делает Angular @for track
function diffByKey(oldList, newList, key) {
  const oldKeys = new Set(oldList.map(x => x[key]));
  const newKeys = new Set(newList.map(x => x[key]));
  const removed = oldList.filter(x => !newKeys.has(x[key]));
  const added   = newList.filter(x => !oldKeys.has(x[key]));
  const reused  = newList.filter(x => oldKeys.has(x[key]));
  return { removed, added, reused };
}

const before = [{id:1},{id:2},{id:3}];
const after  = [{id:2},{id:3},{id:4}];
const r = diffByKey(before, after, 'id');

console.log('удалить:', r.removed.map(x => x.id)); // [1]
console.log('создать:', r.added.map(x => x.id));   // [4]
console.log('переиспользовать:', r.reused.map(x => x.id)); // [2,3]

Частые ошибки

  • Забыть track. В @for он обязателен — без него код не скомпилируется.
  • Использовать $index как track при перестановках. Если порядок меняется, ключом должен быть стабильный id, а не индекс.
  • Тяжёлые выражения в теле цикла. Они выполнятся для каждого элемента на каждой проверке.

Best practices

  • Трекайте по уникальному и стабильному полю — обычно id или uuid.
  • Используйте @empty вместо отдельного @if для пустого состояния.
  • Не считайте, что элементы пересоздаются: при совпадении track сохраняется состояние DOM (фокус, ввод).

Итоги. @for ... track рисует и эффективно обновляет списки, @empty ловит пустоту, контекстные переменные дают индекс и позицию. Track — ключ к производительности. Дальше — слой сервисов и DI.

Закрепляем

Рендеринг списков — повседневная работа, и здесь главный герой это track. Без него Angular не смог бы понять, какой элемент DOM соответствует какому элементу данных после изменения массива, и был бы вынужден пересоздавать весь список целиком. Это не только медленно, но и сбрасывает состояние: фокус ввода, развёрнутые блоки, проигрываемое видео. С правильным track по стабильному идентификатору фреймворк точечно добавляет, удаляет и переставляет узлы, переиспользуя то, что не изменилось.

Выбирайте ключ для track вдумчиво. Идеален уникальный и стабильный идентификатор записи — обычно id или uuid из вашей модели данных. Использовать $index как ключ можно лишь для статичных списков, которые никогда не переупорядочиваются: при перестановке элементов индекс «съезжает», и Angular перепутает узлы, что приведёт к странным визуальным багам. Если в данных нет естественного идентификатора, всерьёз подумайте о том, чтобы его добавить — это окупится и в производительности, и в корректности.

КонструкцияНазначение
@for (x of list; track x.id)Цикл с ключом
@emptyКонтент при пустом списке
$index, $first, $lastКонтекстные переменные
$even, $oddЧётность позиции
Проверьте себя
1. Зачем в @for обязателен параметр track?
AДля сортировки
BЧтобы Angular опознавал элементы между перерисовками и переиспользовал DOM вместо полного пересоздания
CЧтобы ограничить число элементов
DЭто просто синтаксическое требование без эффекта
2. Что делает блок @empty внутри @for?
AОчищает массив
BРендерится, когда коллекция пуста
CУдаляет первый элемент
DСоздаёт пустой элемент в конце