Проблема, которая знакома каждому Angular-разработчику
Вы когда-нибудь пытались вывести в шаблоне Angular содержимое обычного JavaScript-объекта с помощью *ngFor и вдруг понимали: "А как мне достать и ключ, и значение?" Ведь *ngFor по умолчанию работает только с массивами. А объект — это не массив.
Этот вопрос — один из самых популярных на Stack Overflow (тысячи просмотров и десятки ответов). Разработчики ищут не просто решение, а правильное решение — то, которое не убьёт производительность приложения, особенно когда объект большой или часто обновляется.
В этой статье мы не просто дадим ответ. Мы разберём все основные подходы, от простых до продвинутых, и покажем, как писать код, который будет работать быстро и стабильно. Поехали!
Почему *ngFor не работает с объектами "из коробки"?
Директива *ngFor в Angular предназначена для итерируемых структур (Iterable). Массив — это итерируемая структура. Объект — нет. Когда вы пишете:
<div *ngFor="let item of myObject">{{ item }}</div>
Angular выдаст ошибку: "Cannot find a differ supporting object '...'". Angular просто не знает, как перебрать объект.
Но на практике нам часто нужно вывести данные из объекта: конфиги, словари, ответы API, где ключи имеют смысл (например, { name: 'Анна', age: 25, city: 'Москва' }).
Самый простой способ: Object.keys() и Object.entries()
Самый популярный и очевидный ответ на Stack Overflow — преобразовать объект в массив прямо в шаблоне или в компоненте.
Вариант 1: Object.keys() — получаем только ключи
// В компоненте (TypeScript)
export class MyComponent {
myObject = { name: 'Анна', age: 25, city: 'Москва' };
}
<!-- В шаблоне -->
<div *ngFor="let key of objectKeys(myObject)">
Ключ: {{ key }}, Значение: {{ myObject[key] }}
</div>
Но тут есть нюанс: objectKeys должна быть доступна в шаблоне. Либо делаем её методом компонента, либо выносим в отдельное свойство:
// Компонент
export class MyComponent {
myObject = { name: 'Анна', age: 25, city: 'Москва' };
objectKeys = Object.keys; // просто ссылка на нативную функцию
}
Это работает, но вызывает функцию при каждой проверке изменений (change detection). Если объект большой — это удар по производительности.
Вариант 2: Object.entries() — ключи и значения сразу
// Компонент
export class MyComponent {
myObject = { name: 'Анна', age: 25, city: 'Москва' };
get entries() {
return Object.entries(this.myObject);
}
}
<!-- Шаблон -->
<div *ngFor="let entry of entries">
Ключ: {{ entry[0] }}, Значение: {{ entry[1] }}
</div>
Или с деструктуризацией (более читаемо):
<div *ngFor="let [key, value] of entries">
Ключ: {{ key }}, Значение: {{ value }}
</div>
Плюсы: просто, понятно, наглядно.
Минусы: метод entries будет вызываться при каждом цикле обнаружения изменений. Для статичных объектов — ок, для динамических — может стать проблемой.
Производительность: почему это важно?
Представьте, что ваш объект содержит 10 000 записей. Или он обновляется каждые 100 мс (например, данные с сервера в реальном времени). Каждый вызов Object.entries() создаёт новый массив. Angular при каждом цикле обнаружения изменений увидит "новый" массив и перерисует весь список. Это приводит к дёрганью интерфейса, потере фокуса на инпутах и тормозам.
Поэтому для серьёзных проектов нужно использовать оптимизированные подходы.
Правильный путь: KeyValuePipe (но с оговорками)
Angular предоставляет встроенный пайп KeyValuePipe. Он преобразует объект в массив пар { key, value } прямо в шаблоне:
<div *ngFor="let item of myObject | keyvalue">
Ключ: {{ item.key }}, Значение: {{ item.value }}
</div>
Это удобно, лаконично и работает "из коробки". Но!
Важное предупреждение: KeyValuePipe в некоторых версиях Angular (особенно старых) может считаться impure (нечистым). Это значит, что он будет вызываться каждый раз при любом изменении в приложении. Для больших объектов это всё ещё проблема.
В Angular 17+ поведение улучшили, но всё равно стоит быть внимательным. Если объект статичен или меняется редко — KeyValuePipe — ваш выбор. Если объект гигантский — читайте дальше.
Оптимизация №1: trackBy — спасатель производительности
Независимо от того, какой способ вы выбрали, всегда используйте trackBy в *ngFor. Это директива, которая помогает Angular понимать, какие элементы изменились, а какие нет. Без неё Angular удаляет все DOM-элементы и создаёт их заново при любом изменении массива.
// В компоненте
trackByKey(index: number, item: any): string {
return item.key; // уникальный ключ
}
<!-- В шаблоне -->
<div *ngFor="let item of myObject | keyvalue; trackBy: trackByKey">
{{ item.key }}: {{ item.value }}
</div>
Это резко снижает количество перерисовок. Angular будет переиспользовать существующие DOM-элементы, если ключ не изменился.
Оптимизация №2: Мемоизация (кэширование) с помощью pure-пайпа
Если вы не хотите использовать KeyValuePipe (или он вам не подходит), создайте собственный pure-пайп. Pure-пайп вызывается только тогда, когда меняется входное значение (по ссылке), а не при каждом цикле обнаружения изменений.
// key-value-pure.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'keyValuePure',
pure: true // по умолчанию true, но явно указываем
})
export class KeyValuePurePipe implements PipeTransform {
transform(obj: any): { key: string; value: any }[] {
if (!obj) return [];
return Object.keys(obj).map(key => ({ key,