Angular
Як влаштований Angular-застосунок, як фреймворк дізнається про зміни, як працювати з RxJS і що змінилося з Angular 13 до актуальної версії 22. На співбесіді це перевіряють і як знання основ, і як готовність підтримувати або оновлювати старий застосунок.
Перед цією темою: JavaScript і TypeScript.
З чого складається Angular-застосунок?
Section titled “З чого складається Angular-застосунок?”Що це
Angular-застосунок — дерево компонентів. Кореневий компонент запускається в main.ts, решта вкладені в його шаблон.
- Компонент — клас із декоратором
@Componentі HTML-шаблон. Клас зберігає дані й обробники, шаблон їх показує. - Сервіс — клас зі спільною логікою: виклики API, кеш, поточний користувач.
- Маршрутизатор — показує компонент, який відповідає URL.
Як користуватися
ng generate component ordersстворює компонент.- Дочірні компоненти й директиви, потрібні шаблону, перелічіть в
importsдекоратора. - Сервіс отримайте полем класу:
private orders = inject(OrderService).
Типова помилка
Забути додати дочірній компонент в imports. Збірка впаде з помилкою 'app-x' is not a known element.
Як влаштовано
| Синтаксис у шаблоні | Що робить |
|---|---|
{{ total }} |
інтерполяція: значення з класу в текст |
[disabled]="busy" |
прив’язка властивості: клас → DOM |
(click)="save()" |
прив’язка події: DOM → клас |
[(value)]="name" |
двостороння: властивість плюс подія valueChange |
- Директива змінює поведінку елемента:
[class.active],ngModel. Компонент — це директива з власним шаблоном. - Структурні директиви
*ngIfі*ngForз Angular 17 замінені вбудованими блоками@ifі@for. - NgModule проти standalone. В Angular 13 компонент оголошували в
declarationsякогось NgModule. Із v19 компоненти standalone за замовчуванням і перелічують залежності у своїхimports.
Коли що обирати
| Спосіб отримати сервіс | Коли |
|---|---|
inject(Service) у полі |
за замовчуванням, так радить style guide Angular |
| параметр конструктора | старий код; працює як раніше |
Пастки
inject()працює лише в injection context: ініціалізатори полів, конструктор, фабрики провайдерів, гарди. Виклик в обробнику події дасть помилку NG0203.- Сервіс у
providersкомпонента — окремий екземпляр на кожен екземпляр компонента, а не спільний.
Правило
Нові компоненти — standalone, сервіси — providedIn: 'root', залежності — через inject().
Під капотом
- AOT-компілятор перетворює шаблон на функції створення й оновлення DOM. Помилки типів у прив’язках видно під час збірки.
- Пошук провайдера йде знизу вгору: спершу інжектори елементів від компонента до кореня дерева, потім environment-інжектори — маршруту, кореневий, платформний. Не знайшов —
NullInjectorError.
Порівняння з ASP.NET Core
| Angular | Найближчий аналог в ASP.NET Core |
|---|---|
providedIn: 'root', @Service() (v22) |
AddSingleton |
providers маршруту |
екземпляр на розділ застосунку; прямого аналога немає |
providers компонента |
екземпляр на компонент і його піддерево; scopes у .NET не ієрархічні |
inject(X, { optional: true }) |
GetService замість GetRequiredService |
Компроміси
- Standalone робить залежності компонента явними й дозволяє ліниво вантажити окремий компонент. NgModule підтримується, а для переведення є міграція
ng generate @angular/core:standalone. @Service()з v22 — короткий запис@Injectable({ providedIn: 'root' }). Він працює лише зinject(), без параметрів конструктора.inject()спрощує успадкування: базовому класу не потрібенsuper(...)з довгим списком залежностей.- Ієрархія дозволяє дати сторінці власний стан через
providers, але той самий механізм непомітно плодить дублікати синглтонів.
Як перевірити
Angular DevTools у dev-збірці показує дерево компонентів та інжекторів і звідки прийшов кожен сервіс. Дублікати синглтонів видно там само.
Підсумок: DI в Angular ієрархічний: де оголошено провайдер, там і межа спільного екземпляра.
import { Component, Injectable, inject } from '@angular/core';
@Injectable({ providedIn: 'root' }) // один екземпляр на застосунок, як AddSingletonexport class GreetingService { greet(name: string) { return `Hello, ${name}!`; }}
@Component({ selector: 'app-greeting', // standalone за замовчуванням із v19 templateUrl: './greeting.html',})export class Greeting { private readonly greetings = inject(GreetingService); // замість параметра конструктора protected message = '';
protected say(name: string) { this.message = this.greetings.greet(name); }}<input #name value="Angular" /><button (click)="say(name.value)">Greet</button><p [class.empty]="!message">{{ message }}</p> <!-- після кліку: Hello, Angular! -->- Чим
providedIn: 'root'відрізняється відprovidersу компоненті? — Перший дає один екземпляр на застосунок, другий — власний екземпляр на кожен екземпляр компонента. - Де можна викликати
inject()? — В injection context: ініціалізатори полів, конструктор, фабрики, гарди й інтерцептори; інакше NG0203. - Чи потрібен NgModule в новому застосунку? — Ні: із v19 компоненти standalone за замовчуванням, NgModule лишається для старого коду.
- Чим компонент відрізняється від директиви? — Компонент — директива з власним шаблоном; директива змінює поведінку наявного елемента.
- Що означає tree-shakable провайдер? — За
providedIn: 'root'сервіс, який ніде не впроваджено, не потрапить у бандл.
Як працює виявлення змін: zone.js, OnPush, signals і zoneless?
Section titled “Як працює виявлення змін: zone.js, OnPush, signals і zoneless?”Що це
Change detection — перевірка, чи збігається те, що на екрані, з даними компонентів. Якщо значення в прив’язці змінилося, Angular оновлює цей шматок DOM.
Що змінилося з Angular 13
- Тоді перевірку запускав zone.js після кожного кліку, таймера чи HTTP-відповіді — по всьому дереву.
- Тепер стан тримають у сигналах:
signal(0)створює значення,count()читає його,count.set(1)змінює. Angular знає, які шаблони читають сигнал, і оновлює лише їх. - Із v21 нові застосунки працюють без zone.js, із v22 компоненти за замовчуванням OnPush.
Типова помилка
Змінювати масив усередині сигналу: items().push(x). Посилання те саме, сигнал не повідомить про зміну. Потрібно items.update(list => [...list, x]).
Як влаштовано із zone.js
- zone.js підміняє асинхронні API браузера:
setTimeout,Promise,addEventListener, XHR. - Коли задача та її мікрозадачі завершилися,
NgZoneповідомляє про це, і Angular викликаєApplicationRef.tick(). - Дерево компонентів перевіряється згори донизу: кожен вираз шаблону порівнюється з минулим значенням.
- У dev-режимі другий прохід ловить значення, що змінилися під час перевірки, —
ExpressionChangedAfterItHasBeenCheckedError.
Стратегії
| Стратегія | Коли компонент перевіряється |
|---|---|
Eager (раніше Default) |
у кожному циклі |
OnPush (за замовчуванням із v22) |
новий input за посиланням, подія в піддереві, markForCheck(), зміна прочитаного сигналу |
Сигнали
signal зберігає значення, computed обчислює похідне — ліниво й з кешем, effect виконує побічну дію: лог, запис у localStorage, виклик сторонньої бібліотеки.
Пастки
- Мутація об’єкта у вході OnPush-компонента: посилання те саме, перевірки не буде.
- Поле, змінене в
setTimeoutчи в підписці, в OnPush-компоненті не відмалюється без сигналу абоmarkForCheck(). effect, який копіює один сигнал в інший. Для похідних значень єcomputedіlinkedSignal.
Правило
Стан — у сигналах, входи — незмінні, похідне — через computed.
Під капотом
- Сигнали утворюють граф: шаблони,
computedіeffect— споживачі, сигнали — джерела. Читання сигналу в реактивному контексті реєструє залежність. set()позначає залежні представлення й планує перевірку. OnPush-предки слугують лише шляхом до них і самі не перевіряються повторно.- Без zone.js перевірку запускають лише сповіщення:
markForCheck()(його викликаєasyncpipe),ComponentRef.setInput, зміна сигналу з шаблону, обробник події в шаблоні.
Ціна zone.js
- Перевірка після будь-якої асинхронної події, навіть якщо дані не змінювалися.
- Вага бандла, час старту й довгі стек-трейси.
- Нативний
async/awaitzone.js перехопити не може, тому збірка переписує його в генератори.
Компроміси під час міграції
ng updateдо v21 додаєprovideZoneChangeDetection(), до v22 —ChangeDetectionStrategy.Eagerкомпонентам без явної стратегії. Поведінка зберігається.- Компоненти переводять на OnPush-сумісний код: сигнали, незмінні входи,
asyncpipe. NgZone.onStableіonMicrotaskEmptyу zoneless не спрацьовують — їх замінюють наafterNextRender.- Останніми прибирають
provideZoneChangeDetection()іzone.jsзpolyfills.
Як перевірити
- У dev-збірці
provideCheckNoChangesConfig({ exhaustive: true, interval: 1000 })знаходить прив’язки, змінені без сповіщення. - Профайлер Angular DevTools показує тривалість кожного циклу й перевірені компоненти.
Підсумок: zoneless — перехід від «перевіряти все після будь-якої події» до «оновлювати те, про що повідомили», і готовність до нього визначає код компонентів, а не прапорець.
import { ChangeDetectionStrategy, Component, computed, effect, signal } from '@angular/core';
@Component({ selector: 'app-cart', templateUrl: './cart.html', // Із v22 OnPush — за замовчуванням; в Angular 13 його вказували явно changeDetection: ChangeDetectionStrategy.OnPush,})export class Cart { protected readonly items = signal<{ name: string; price: number }[]>([]); // Перерахується лише в разі зміни items protected readonly total = computed(() => this.items().reduce((sum, i) => sum + i.price, 0));
constructor() { effect(() => console.log('Items:', this.items().length)); // побічна дія, не стан }
protected add() { // Нове посилання: сигнал повідомить про зміну. // this.items().push(...) не спрацює — посилання те саме this.items.update(list => [...list, { name: 'Book', price: 10 }]); }}<button (click)="add()">Add</button><p>Items: {{ items().length }}, total: {{ total() }}</p> <!-- після кліку: Items: 1, total: 10 -->import { bootstrapApplication } from '@angular/platform-browser';import { provideZoneChangeDetection, provideZonelessChangeDetection } from '@angular/core';import { App } from './app/app';
// Angular 20: zoneless вмикають явно; із v21 це поведінка за замовчуваннямbootstrapApplication(App, { providers: [provideZonelessChangeDetection()] });
// Старий застосунок після ng update до v21+ лишається на zone.js так:// bootstrapApplication(App, { providers: [provideZoneChangeDetection()] });- Чому OnPush-компонент не оновився після
this.user.name = 'X'? — Посилання на об’єкт не змінилося; потрібен новий об’єкт або сигнал. - Чим
computedвідрізняється відeffect? —computedповертає значення й кешується,effectлише виконує побічну дію. - Що означає
ExpressionChangedAfterItHasBeenCheckedError? — Dev-перевірка знайшла прив’язку, яка змінилася під час циклу перевірки. - Що потрібно, щоб застосунок працював без zone.js? — Сповіщати Angular через сигнали,
markForCheckабоasyncpipe і не покладатися наNgZone.onStable. - Чи потрібен
NgZone.runOutsideAngularу zoneless? — Ні, але виклик нешкідливий; у zone-застосунку він прибирає зайві перевірки на частих подіях.
Як працювати з RxJS в Angular?
Section titled “Як працювати з RxJS в Angular?”Що це
Observable — джерело, яке після підписки видає значення: нуль, одне або багато, а потім завершується чи падає з помилкою.
| Ознака | Promise | Observable |
|---|---|---|
| Коли починається робота | одразу під час створення | під час підписки |
| Скільки значень | одне | скільки завгодно |
| Скасування | немає | відписка (unsubscribe) |
Як користуватися
http.get<User[]>('/api/users')повертає Observable, запит піде під час підписки.- У шаблоні:
users$ | async— підписка й відписка автоматично. - У коді:
.subscribe(users => ...).
Типова помилка
Підписатися на потік, який сам не завершується (interval, router.events, Subject у сервісі), і не відписатися. Компонент знищено, а підписка живе й тримає його в пам’яті.
Як влаштовано
HttpClientвидає одне значення й завершується. Кожна підписка — новий запит, відписка до відповіді скасовує його.- Оператори в
pipe()будують новий потік:map,filter,debounceTime,catchError.
Коли що обирати
| Оператор | Нове значення, поки минуле не завершено | Приклад |
|---|---|---|
switchMap |
скасовує минуле | пошук під час введення |
mergeMap |
виконує паралельно | завантаження незалежних файлів |
concatMap |
ставить у чергу | збереження, де важливий порядок |
exhaustMap |
ігнорує нове | кнопка «Надіслати», вхід |
Як відписуватися
asyncpipe — у шаблоні.toSignal(obs$)перетворює потік на сигнал і відписується під час знищення компонента;toObservable(sig)робить зворотне.takeUntilDestroyed()(стабільний із v19) — для ручногоsubscribe. Поза injection context йому передаютьDestroyRef.
Пастки
- Два
asyncpipe на один HTTP-потік — два запити. Спільний результат — черезshareReplayабоtoSignal. takeUntil(destroy$)не останнім уpipe(): оператори після нього можуть лишити підписку живою.- Вкладений
subscribeусерединіsubscribeзамістьswitchMap: підписки не скасовуються, помилки губляться.
Правило
Підписку створює шаблон або toSignal; ручний subscribe — лише з takeUntilDestroyed().
Під капотом
- Observable — функція підписки.
pipe()нічого не запускає, аsubscribe()будує ланцюжок підписників до джерела. - Відписка йде ланцюжком угору. Для
HttpClientце скасування запиту:AbortControllerу fetch-бекенда (за замовчуванням із v22) абоabort()у XHR. toObservableвіддає значення черезeffect, тому кількаset()поспіль дадуть одне значення — останнє.
Ціна
switchMapскасовує відповідь на клієнті, але не роботу на сервері. Для POST і PUT це втрачені чи задвоєні зміни, тому запис — черезconcatMapабоexhaustMap.mergeMapбез ліміту конкурентності (mergeMap(fn, 4)) здатен надіслати сотні запитів разом.shareReplay(1)безrefCount: trueтримає підписку на джерело й після відходу всіх підписників.
Компроміси
| Задача | Інструмент |
|---|---|
| стан для шаблону | сигнал |
| завантаження даних за параметрами | httpResource, rxResource (стабільні із v22) |
| події в часі, WebSocket, debounce, retry | RxJS |
Як перевірити
- Витік видно в Chrome DevTools: знімок купи після кількох переходів туди й назад показує від’єднані (detached) елементи й живі екземпляри компонента.
- На вкладці Network скасовані запити позначені
(canceled), а дублікати запитів видають зайву підписку.
Підсумок: RxJS — інструмент для потоків подій і конкурентності, а не для зберігання стану; стан — у сигналах.
import { Component, inject } from '@angular/core';import { HttpClient } from '@angular/common/http';import { FormControl, ReactiveFormsModule } from '@angular/forms';import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';import { debounceTime, distinctUntilChanged, interval, switchMap } from 'rxjs';
@Component({ selector: 'app-product-search', imports: [ReactiveFormsModule], templateUrl: './product-search.html' })export class ProductSearch { private readonly http = inject(HttpClient); protected readonly query = new FormControl('', { nonNullable: true });
// switchMap скасовує минулий запит, якщо користувач продовжив друкувати protected readonly products = toSignal( this.query.valueChanges.pipe( debounceTime(300), distinctUntilChanged(), switchMap(q => this.http.get<{ id: number; name: string }[]>('/api/products', { params: { q } })), ), { initialValue: [] }, // відписка — автоматично під час знищення компонента );
constructor() { // Нескінченний потік: без takeUntilDestroyed підписка переживе компонент interval(60_000).pipe(takeUntilDestroyed()).subscribe(n => console.log('tick', n)); }}<input [formControl]="query" placeholder="Search" />@for (p of products(); track p.id) { <p>{{ p.name }}</p>}- Чим Observable відрізняється від Promise? — Лінивий, багатозначний і скасовуваний; Promise виконується одразу й дає одне значення.
- Який оператор узяти для кнопки «Зберегти»? —
exhaustMap, щоб ігнорувати повторні кліки, абоconcatMap, якщо важливий кожен клік по черзі. - Чи треба відписуватися від
HttpClient? — Він завершується сам, але відписка скасовує запит і не дає колбеку спрацювати на знищеному компоненті. - Чим
toSignalвідрізняється відasyncpipe? — Дає сигнал, який можна читати в коді та вcomputed; обидва відписуються автоматично. - Чому
takeUntilставлять останнім? — Оператори після нього, наприкладswitchMap, можуть тримати внутрішню підписку після сигналу зупинки.
Що змінилося в Angular з версії 13 до актуальної?
Section titled “Що змінилося в Angular з версії 13 до актуальної?”Що це
Angular 13 вийшов у листопаді 2021, поточна версія — 22 (червень 2026). За цей час змінився сам стиль коду.
Головне
- Компоненти без NgModule — standalone.
- Новий синтаксис шаблонів:
@if,@for,@switchзамість*ngIfі*ngFor. @defer— ліниве завантаження частини шаблону.- Сигнали: стан, входи
input()і виходиoutput(). inject()замість параметрів конструктора.- Збірка на esbuild і Vite — помітно швидша за Webpack.
- SSR з гідратацією — пакет
@angular/ssrзамість Angular Universal. - Робота без zone.js і OnPush за замовчуванням.
Типова помилка
Думати, що старий код перестав працювати. NgModule, @Input() і *ngIf (deprecated із v20) підтримуються, а ng update сам виправляє те, що ламається.
Коли що з’явилося
| Версія | Що змінилося |
|---|---|
| 14–15 (2022) | standalone-компоненти, типізовані форми, inject(), функціональні гарди й інтерцептори |
| 16 (травень 2023) | сигнали (preview), takeUntilDestroyed, гідратація (preview), збірка на esbuild (preview) |
| 17 (листопад 2023) | @if, @for, @defer; стабільні сигнали; application builder і Vite для нових проєктів |
| 17.1–19 | input(), output(), model(), сигнальні запити; із v19 стабільні, standalone за замовчуванням |
| 20 (травень 2025) | стабільні effect, linkedSignal, toSignal; *ngIf і *ngFor deprecated; zoneless стабільний із 20.2 |
| 21 (листопад 2025) | zoneless і Vitest за замовчуванням у нових проєктах; експериментальні Signal Forms |
| 22 (червень 2026) | OnPush за замовчуванням; стабільні Signal Forms, resource і httpResource; @Service(); HttpClient на fetch |
Пастки міграції
ng updateпереходить лише на одну мажорну версію за раз. З 13 до 22 — дев’ять кроків, у кожного свої вимоги до Node.js і TypeScript.- Сторонні бібліотеки оновлюють разом з Angular: Kendo UI for Angular, angular-oauth2-oidc та інші мають підтримувати цільову версію.
- Власні налаштування Webpack в application builder не переносяться.
Правило
Спершу довести версії до актуальної зі збереженням поведінки, потім модернізувати код міграціями ng generate @angular/core:... — по одній за раз.
Чому так
- Сигнали й zoneless прибирають головну ціну старого Angular — перевірку всього дерева на будь-яку подію.
- Standalone і
@deferроблять одиницею завантаження компонент, а не модуль. - esbuild і Vite пришвидшують збірку; Webpack-збирачі у v22 оголошено deprecated.
- Із v22 мажорний реліз виходить раз на 12 місяців, а не раз на пів року. v23 очікується близько червня 2027.
Стратегія міграції
- Оновлювати мажор за мажором за update guide, після кожного кроку — збірка й тести.
- Автоматичні міграції зберігають поведінку: у v21 застосунку додається
provideZoneChangeDetection(), у v22 компонентам без стратегії —ChangeDetectionStrategy.Eager. - Збирач переводять командою
ng update @angular/cli --name use-application-builder. - Далі по одній: standalone, control flow,
inject(),input()іoutput(), ліниві маршрути. - Останніми — OnPush і zoneless: вони змінюють логіку, а не синтаксис.
Ціна
- Кожна мажорна версія піднімає мінімум TypeScript і Node.js: v22 вимагає TypeScript 6.0 і Node.js 22.22+ або 24.13+.
- Видаляються старі API: у v22 —
ComponentFactoryResolver.HttpClientперейшов на fetch, і для прогресу завантаження файлів потрібенwithXhr(). - Станом на жовтень 2026 Kendo UI for Angular 25.x підтримує Angular 20–22, а angular-oauth2-oidc 22.x вимагає Angular 22.
Компроміси
Переписувати все одразу немає потреби: старий і новий стиль співіснують в одному застосунку. Нове пишуть у новому стилі, старе переводять під час змін.
Підсумок: міграція з 13 — спершу безпечне оновлення версій зі збереженням поведінки, потім поступовий перехід на сигнали й zoneless.
import { Component, EventEmitter, Input, Output, inject, input, output } from '@angular/core';import { HttpClient } from '@angular/common/http';
interface User { id: number; name: string; }
// Angular 13: декоратори, конструктор, оголошення в declarations якогось NgModule@Component({ selector: 'app-user-old', templateUrl: './user-old.component.html' })export class UserOldComponent { @Input() user!: User; @Output() selected = new EventEmitter<User>(); constructor(private http: HttpClient) {}}
// Angular 22: standalone, сигнальні входи й виходи, inject()@Component({ selector: 'app-user', templateUrl: './user.html' })export class UserCard { readonly user = input.required<User>(); // читається як user() readonly selected = output<User>(); private readonly http = inject(HttpClient);}<!-- Angular 13 --><li *ngFor="let o of orders; trackBy: trackById">{{ o.title }}</li><p *ngIf="!orders.length">No orders</p>
<!-- Angular 22: track обов'язковий, @empty замість окремого *ngIf -->@for (o of orders(); track o.id) { <li>{{ o.title }}</li>} @empty { <p>No orders</p>}
<!-- Код графіка завантажиться, коли блок з'явиться в області видимості -->@defer (on viewport) { <app-sales-chart />} @placeholder { <p>Chart</p>}- Чи можна змішувати NgModule і standalone? — Так: standalone-компонент імпортують у NgModule, а NgModule — в
importsкомпонента. - Що робить
trackв@for? — Задає ключ, за яким Angular зіставляє елементи з DOM; в@forвін обов’язковий. - Чим
@deferвідрізняється від лінивого маршруту? — Відкладає завантаження частини шаблону за тригером (viewport, idle, interaction), а не цілої сторінки. - Чому не можна оновитися з 13 одразу на 22? —
ng updateпідтримує перехід лише на одну мажорну версію, міграції розраховані на крок. - Що таке гідратація? — Клієнт перевикористовує DOM, відмальований на сервері, а не будує його заново.
Як будувати великий Angular-застосунок і керувати станом?
Section titled “Як будувати великий Angular-застосунок і керувати станом?”Що це
Великий застосунок ділять за фічами — бізнес-розділами: замовлення, клієнти, звіти.
core/— те, що потрібне один раз: авторизація, інтерцептори, каркас сторінки.shared/— спільні UI-компоненти й пайпи без бізнес-логіки.features/orders/— маршрути, сторінки й стан однієї фічі.
Як користуватися
Фічу підключають лінивим маршрутом: loadChildren: () => import('./orders/orders.routes'). Її код потрапляє в окремий файл (чанк) і завантажується під час першого переходу.
Типова помилка
Статично імпортувати компонент лінивої фічі в іншому місці, наприклад в app.ts. Тоді він потрапить в основний бандл, і ліниве завантаження зникне.
Коли що обирати
| Підхід до стану | Коли |
|---|---|
| сервіс із сигналами | стан однієї фічі, прості операції |
| NgRx SignalStore | багато сторів, потрібна єдина структура й плагіни: withEntities, події |
| NgRx Store | спільний стан багатьох фіч, складні потоки подій, Redux DevTools |
Продуктивність
- OnPush і сигнали — менше перевірок.
trackзаidв@for— DOM перевикористовується, а не створюється заново.- Довгі списки — віртуальне прокручування:
cdk-virtual-scroll-viewportз Angular CDK абоscrollable="virtual"у Kendo Grid. - Важкі віджети — графіки, планувальник — в
@defer.
Пастки
providedIn: 'root'для стану сторінки: він переживає відхід зі сторінки, і після повернення видно старі дані. Такий стор оголошують уprovidersмаршруту чи компонента.- Глобальний стор для всього підряд, включно зі станом однієї форми.
- Kendo Grid на тисячі рядків без пейджингу чи віртуального прокручування.
Правило
Починати із сервісу на сигналах. SignalStore — коли сторів багато й потрібна єдина форма, NgRx Store — коли стан справді спільний і складний.
Компроміси в стані
- Більша частина «стану» — кеш серверних даних. Його закривають
httpResourceабо сервіс; глобальний стор потрібен для спільного: користувач, права, налаштування. - NgRx Store дає трасування й time travel у Redux DevTools ціною шаблонного коду: action, reducer, selector і effect на операцію.
providersмаршруту дають стору час життя фічі — як scope в ASP.NET Core, лише на розділ застосунку.
Ціна розміру бандла
budgetsвangular.jsonвалять збірку, якщо початковий бандл перевищив ліміт.- Склад бандла показують
ng build --stats-jsonі аналізатор esbuild. - Kendo UI і Angular Material додають ваги: імпортують лише потрібні компоненти, а Chart і Scheduler ховають за
@defer.
Бібліотеки компонентів
- Оновлення Angular обмежене їхніми
peerDependencies— перевіряють доng update. - Власна обгортка на кшталт
app-data-gridповерх Kendo Grid локалізує налаштування й майбутню заміну бібліотеки.
Авторизація в SPA
Функціональний інтерцептор додає access-токен, гард canMatch ховає недоступні маршрути. Гард — зручність для користувача, права перевіряє API. Де зберігати токени й коли потрібен BFF — у темі OAuth 2.0 і OpenID Connect.
Як перевірити в продакшені
Core Web Vitals реальних користувачів і Lighthouse показують підсумок, вкладка Coverage у Chrome — скільки завантаженого JavaScript не виконалося.
Підсумок: масштаб тримають межі фіч, ліниве завантаження й усвідомлений вибір, де живе стан.
import { Routes } from '@angular/router';import { inject } from '@angular/core';import { AuthService } from './core/auth.service';import { OrdersStore } from './features/orders/orders.store';
export const routes: Routes = [ { path: '', loadComponent: () => import('./features/home/home').then(m => m.Home) }, { path: 'orders', canMatch: [() => inject(AuthService).isLoggedIn()], // зручність; права перевіряє API providers: [OrdersStore], // один стор на фічу, живе, поки відкрито розділ // Окремий чанк: завантажиться під час першого переходу на /orders loadChildren: () => import('./features/orders/orders.routes').then(m => m.ORDERS_ROUTES), },];import { computed, inject } from '@angular/core';import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';import { Order, OrdersApi } from './orders.api';
export const OrdersStore = signalStore( withState({ orders: [] as Order[], filter: '', loading: false }), withComputed(({ orders, filter }) => ({ // Похідний стан — звичайний computed visible: computed(() => orders().filter(o => o.customer.includes(filter()))), })), withMethods((store, api = inject(OrdersApi)) => ({ setFilter(filter: string) { patchState(store, { filter }); }, async load() { patchState(store, { loading: true }); patchState(store, { orders: await api.loadAll(), loading: false }); }, })),);- Де оголосити стор сторінки? — У
providersмаршруту чи компонента, щоб він зникав разом із розділом. - Коли NgRx Store виправданий? — Багато фіч ділять складний стан, потрібні строгий односпрямований потік і Redux DevTools.
- Чому імпорт із лінивої фічі може зламати ліниве завантаження? — Статичний імпорт тягне модуль в основний бандл.
- Чи захищає гард дані? — Ні, це UX: код SPA доступний користувачу, права перевіряє API.
- Як пришвидшити таблицю на 10 000 рядків? — Пейджинг або віртуальне прокручування, OnPush і
trackзаid.