1. Знакомство с async pipe
В Angular мы часто работаем с Observable — например, когда получаем данные с сервера:
this.users$ = this.http.get<User[]>('/api/users');
В шаблоне хочется просто взять и вывести список пользователей. Но Observable — это не массив, а "поток данных", и чтобы получить значения, нам обычно нужно подписаться:
this.users$.subscribe(users => {
this.users = users;
});
А потом не забыть отписаться в ngOnDestroy:
ngOnDestroy() {
this.subscription.unsubscribe();
}
Согласитесь, это утомляет! Особенно если таких подписок много, и вы постоянно боитесь утечек памяти.
async pipe — это Angular-магия, которая делает всё это за вас:
- Автоматически подписывается на Observable, когда шаблон отображается.
- Автоматически отписывается, когда компонент/директива уничтожается или элемент скрывается через *ngIf, *ngFor и т.д.
- Обновляет значение в шаблоне при каждом новом значении Observable.
Как async pipe выглядит в шаблоне?
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>
</ul>
Всё! Никаких ручных .subscribe(), никаких отписок, никаких хлопот. Просто — Observable | async.
Синтаксис async pipe
Использование async pipe максимально простое:
{{ observable$ | async }}
или в *ngFor:
<div *ngFor="let item of items$ | async">
{{ item }}
</div>
Пояснение:
- observable$ — это Observable (например, Observable<User[]>).
- | async — "пайп" (pipe), который подписывается на Observable и возвращает последнее значение.
Совет: Обозначение переменной с $ (например, users$) — общепринятая практика для названия Observable. Это не обязательно, но очень удобно для чтения кода.
Как работает async pipe «под капотом»?
Когда Angular встречает в шаблоне observable$ | async, он:
- Подписывается на Observable.
- Когда приходит новое значение — обновляет DOM.
- Если компонент уничтожается или элемент исчезает (например, через *ngIf="false"), Angular отписывается от Observable.
- Если Observable изменился (например, вы присвоили в users$ новый Observable) — Angular отписывается от старого и подписывается на новый.
Всё это происходит автоматически! Никаких unsubscribe вручную.
2. Пример: загрузка пользователей с помощью async pipe
Давайте напишем мини-приложение, которое показывает список пользователей, загружая их с сервера.
users.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
}
@Injectable({ providedIn: 'root' })
export class UsersService {
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
}
users.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { UsersService, User } from './users.service';
@Component({
selector: 'app-users',
templateUrl: './users.component.html'
})
export class UsersComponent {
users$: Observable<User[]>;
constructor(private usersService: UsersService) {
this.users$ = this.usersService.getUsers();
}
}
users.component.html
<h2>Пользователи</h2>
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>
</ul>
Всё! Нет ни одной ручной подписки, никакой отписки — Angular всё сделает за вас.
3. async pipe с *ngIf и null-safe оператором
Иногда Observable может вернуть null или данные приходят не сразу. Как быть?
<div *ngIf="users$ | async as users; else loading">
<ul>
<li *ngFor="let user of users">
{{ user.name }}
</li>
</ul>
</div>
<ng-template #loading>
<p>Загрузка...</p>
</ng-template>
Пояснение:
- as users — присваивает значение переменной users внутри блока.
- Пока данные не пришли — отображается блок loading.
4. async pipe с Observables, которые могут завершиться с ошибкой
Если Observable завершился с ошибкой, async pipe просто ничего не покажет. Для обработки ошибок используйте RxJS-оператор catchError:
import { catchError, of } from 'rxjs';
this.users$ = this.usersService.getUsers().pipe(
catchError(error => {
this.error = 'Ошибка загрузки пользователей!';
return of([]); // Возвращаем пустой массив, чтобы шаблон не упал
})
);
И в шаблоне:
<div *ngIf="error; else usersList">
{{ error }}
</div>
<ng-template #usersList>
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>
</ul>
</ng-template>
5. async pipe с Promises
Async pipe работает не только с Observables, но и с Promises!
promiseData = fetchData(); // Возвращает Promise<string>
<p>Результат: {{ promiseData | async }}</p>
Это удобно, если у вас есть асинхронные функции, которые возвращают Promise, например, для совместимости со старыми библиотеками.
6. Сравнение: ручная подписка vs async pipe
Давайте сравним два подхода.
Ручная подписка
export class UsersComponent implements OnInit, OnDestroy {
users: User[] = [];
private sub: Subscription;
ngOnInit() {
this.sub = this.usersService.getUsers().subscribe(users => {
this.users = users;
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
<ul>
<li *ngFor="let user of users">
{{ user.name }}
</li>
</ul>
async pipe
export class UsersComponent {
users$ = this.usersService.getUsers();
}
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>
</ul>
async pipe выигрывает:
- Нет подписок/отписок — меньше кода, меньше багов.
- Не забываете про память.
- Код проще и чище.
7. async pipe и несколько подписок
Если в шаблоне вы используете один и тот же Observable с async pipe в нескольких местах, Angular будет подписываться на него столько раз, сколько вы вызвали | async. Для "горячих" Observable (например, Subject) это не проблема, но для "холодных" (например, HTTP-запрос через .get()) — каждый раз будет новый запрос!
Плохо:
<p>{{ users$ | async | json }}</p>
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>
</ul>
Решение: использовать as-переменную или shareReplay:
<div *ngIf="users$ | async as users">
<p>{{ users | json }}</p>
<ul>
<li *ngFor="let user of users">
{{ user.name }}
</li>
</ul>
</div>
Или кэшировать поток через RxJS:
import { shareReplay } from 'rxjs/operators';
this.users$ = this.usersService.getUsers().pipe(
shareReplay(1)
);
8. Полезные нюансы
async pipe и Change Detection (Обновление данных)
Когда Observable эмиттит новое значение, async pipe автоматически вызывает перерисовку компонента. Это особенно удобно для данных, которые могут меняться со временем (например, WebSocket-потоки, таймеры, формы).
async pipe с switchMap и реактивными формами
Часто нужно динамически обновлять Observable по какому-то событию (например, при вводе текста):
searchTerm$ = new Subject<string>();
results$ = this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.api.search(term))
);
В шаблоне:
<input (input)="searchTerm$.next($event.target.value)">
<ul>
<li *ngFor="let item of results$ | async">{{ item }}</li>
</ul>
async pipe сам обновляет список при каждом новом поисковом запросе.
async pipe и OnPush стратегия обнаружения изменений
Если вы используете ChangeDetectionStrategy.OnPush (для оптимизации производительности), async pipe — ваш лучший друг! Он сам "подскажет" Angular, когда нужно обновить компонент.
async pipe и отписка: почему это важно?
Если вы вручную подписываетесь на Observable, но забыли отписаться — Angular-компонент может висеть в памяти даже после удаления со страницы! Это приводит к утечкам памяти и странным багам.
async pipe всегда отписывается сам, когда компонент уничтожается или элемент исчезает из DOM. Это делает ваш код более надёжным.
async pipe с вложенными Observables
Иногда бывает Observable внутри Observable (например, когда один запрос зависит от другого). Для таких случаев лучше использовать RxJS-операторы (switchMap, combineLatest и т.д.) в TS-коде, а не в шаблоне.
9. Типичные ошибки при использовании async pipe
Ошибка №1: Использование async pipe с не-Observable
Если вы пишете:
{{ user | async }}
а user — просто объект, а не Observable или Promise, Angular выдаст ошибку или ничего не покажет. Используйте async только с Observables/Promises.
Ошибка №2: Многократная подписка в шаблоне
Как уже говорили выше: если вы пишете users$ | async в нескольких местах, будет несколько подписок. Это может привести к повторным HTTP-запросам. Используйте as-переменную или shareReplay.
Ошибка №3: Не обрабатывать ошибки в Observable
Если Observable завершился с ошибкой, async pipe просто ничего не покажет. Используйте catchError в вашем Observable, чтобы не было пустого экрана.
Ошибка №4: Забыли про null при первом рендере
Пока Observable не эмиттит значение, async pipe выдаёт null. Если вы пишете:
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>
</ul>
и не проверяете на null, может быть ошибка. Лучше использовать *ngIf="users$ | async as users".
Ошибка №5: Попытка использовать async pipe вне шаблона
Async pipe работает только в шаблоне Angular, не в TypeScript-коде. В TS-коде используйте .subscribe() или RxJS-операторы.
Ошибка №6: Использование async pipe с Subject без значения
Если вы используете Subject, который ещё не эмиттил значение, async pipe выдаст null. Для BehaviorSubject или ReplaySubject проблем не будет, потому что они всегда хранят последнее значение.
Ошибка №7: Забыли про отписку при ручной подписке
Если вы используете и async pipe, и ручные подписки — не забывайте, что отписываться нужно только от ручных подписок. Async pipe всё делает сам.
Ошибка №8: Использование async pipe с горячими и холодными Observable без учёта их особенностей
Например, HTTP-запрос через .get() — это "холодный" Observable: каждый раз при подписке будет новый запрос. Если вы пишете users$ | async в нескольких местах — будет несколько запросов. Для Subject или BehaviorSubject — это "горячий" Observable, подписки делят данные.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ