Шаблон компонента получает один vm$ — Observable, который содержит всё нужное для рендера: user, route, permissions, loading. Один vm$ | async в шаблоне вместо четырёх async pipe.
Какую проблему решаем
Если шаблон подписан на 4 разных Observable через async pipe — Angular подпишется 4 раза на cold sources (4 HTTP-запроса!). А ещё промежуточные состояния — user уже пришёл, permissions ещё null — шаблон будет рендерить полу-готовый интерфейс.
Операторы и зачем они нужны
combineLatest — собирает все источники в один поток. Каждое изменение любого — новый VM.
map — вычисляет derived state (canAdmin = permissions.includes('admin')) один раз в одном месте.
shareReplay({ bufferSize: 1, refCount: true }) — несколько подписчиков получают один и тот же VM. Late subscriber видит последнее состояние сразу.
Подводные камни
Без shareReplay два vm$ | async в одном шаблоне создадут две подписки на upstream. Двойные HTTP-запросы.
Не смешивайте imperative-поля и vm$. Один источник истины — vm$.
Если какой-то source не имеет initial value, combineLatest молчит до первого эмита каждого. Используйте startWith или BehaviorSubject.
Что в итоге получаем
Шаблон рендерит один согласованный объект. Никаких промежуточных «полу-загруженных» рендеров.