Skip to main content

Окремі компоненти в Angular

Standalone компонент - це компонент, який оголошує свої залежності прямо в @Component, без потреби в NgModule. Доступний з Angular 14, генерується за замовчуванням з Angular 17.

Теорія

TL;DR

  • Встановлюєш standalone: true в @Component і перелічуєш усі залежності шаблону в масиві imports компонента
  • До Angular 14 кожен компонент мав жити в NgModule - standalone прибирає цей шар
  • Запуск змінюється: bootstrapApplication() замість platformBrowserDynamic().bootstrapModule()
  • Ліниве завантаження (lazy loading) стає коротшим: loadComponent завантажує один компонент замість loadChildren з модулем
  • Директиви та пайпи підтримують standalone: true так само, як компоненти

Швидкий приклад

typescript
@Component({ selector: 'app-user-card', standalone: true, imports: [CommonModule, RouterLink], // залежності прямо тут, без NgModule template: ` <div *ngIf="user"> <h2>{{ user.name }}</h2> <a [routerLink]="['/profile', user.id]">Профіль</a> </div> ` }) export class UserCardComponent { @Input() user?: { id: number; name: string }; }

CommonModule і RouterLink імпортуються прямо в компонент. Жодного файлу модуля, жодного масиву declarations. Компонент сам знає, що йому потрібно.

NgModule проти standalone

У модульному Angular кожен компонент має модуль, який його оголошує. Той модуль також імпортує все необхідне для компонента (CommonModule, FormsModule тощо) і реекспортує компонент, щоб інші модулі могли його використовувати. Три файли для простої фічі.

Standalone прибирає цей шар. Масив imports в @Component працює так само, як в @NgModule, але охоплює тільки один компонент. Коли компілятор розв'язує прив'язки в шаблоні, він читає цей масив напряму, а не шукає батьківський модуль.

Коли використовувати standalone

  • Новий Angular-проект: CLI з Angular 17+ генерує standalone за замовчуванням
  • Будь-який новий компонент в існуючому додатку: standalone і модульний код вільно поєднуються
  • Ліниве завантаження одного компонента: loadComponent без обгортки у вигляді feature-модуля
  • Бібліотеки компонентів: менше файлів, чіткіший граф залежностей

Мігрувати існуючий проект можна через ng generate @angular/core:standalone.

Запуск додатку без NgModule

typescript
// main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { provideRouter } from '@angular/router'; import { provideHttpClient } from '@angular/common/http'; import { routes } from './app/app.routes'; bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideHttpClient(), ], });

AppModule зник. Провайдери переїхали до другого аргументу bootstrapApplication. Кожна функція provide* замінює те, що раніше було імпортом модуля. Особисто я знайшов такий підхід значно зручнішим для читання на code review, ніж пошук по масиву imports модуля.

Ліниве завантаження одного компонента

typescript
export const routes: Routes = [ { path: 'settings', loadComponent: () => import('./settings/settings.component').then(m => m.SettingsComponent), }, ];

Раніше для лінивого маршруту потрібен був окремий NgModule з модулем маршрутизації всередині. Тепер сам компонент є межею лінивого завантаження. Менший бандл-сплит, менше коду.

Як компілятор обробляє standalone

Коли Angular бачить standalone: true, він вважає масив imports компонента єдиним джерелом залежностей для шаблону. Кожна директива, пайп або компонент, що використовуються в шаблоні, мають бути в цьому списку або в модулі зі списку. Відсутній імпорт дає помилку під час компіляції (compile error), а не в рантаймі. Це конкретне покращення порівняно зі старою системою модулів, де забутий реекспорт у спільному модулі ламав все тільки під час запуску.

Типові помилки

Забули standalone: true, але прибрали NgModule

typescript
// Не скомпілюється @Component({ selector: 'app-header', // standalone: true відсутній imports: [CommonModule], template: `<nav>...</nav>` }) export class HeaderComponent {}

Angular видає: Component 'HeaderComponent' is not a part of any NgModule. Вирішується одним рядком.

declarations в standalone-компоненті

typescript
// Неправильно - declarations не існує в standalone @Component({ standalone: true, declarations: [ChildComponent], // цієї властивості тут немає }) export class ParentComponent {}

Standalone-компоненти використовують imports для всього. declarations - це концепція модуля.

Занадто широкий імпорт

typescript
// Компілюється, але тягне весь CommonModule imports: [CommonModule] // Точніше - бандлер знає, що можна прибрати imports: [NgIf, NgFor, AsyncPipe]

Обидва варіанти коректні. Другий дає tree-shaking більше інформації про те, що можна видалити з фінального бандлу.

Неправильний метод запуску після видалення AppModule

Якщо видалити AppModule, але залишити platformBrowserDynamic().bootstrapModule(AppModule) в main.ts, додаток не запуститься. Потрібно перейти на bootstrapApplication(AppComponent, { providers: [...] }).

Де standalone зустрічається в реальних проектах

  • Angular 17+ CLI: всі згенеровані компоненти - standalone за замовчуванням
  • Angular Material 15+: всі компоненти бібліотеки постачаються як standalone
  • Nx workspace: standalone за замовчуванням для Angular-бібліотек
  • Офіційна документація Angular і приклад tour-of-heroes: повністю standalone з Angular 17

Питання на співбесіді

Q: Чи можна використовувати standalone-компонент у модульному додатку?
A: Так. Його додають в масив imports NgModule, а не в declarations. Компонент сам управляє своїми залежностями, модулю потрібно лише його перелічити.

Q: Чи може немодульний компонент використовувати standalone?
A: Через модуль - так. Standalone-компонент імпортується у feature-модуль, і той модуль може імпортуватися іншими. Працює, але чистіший шлях - мігрувати батьківський компонент теж.

Q: Що станеться, якщо додати немодульний компонент в imports NgModule?
A: Angular кине помилку компіляції з вимогою спочатку додати standalone: true. Тихої помилки не буде.

Q: Чи обов'язковий standalone: true в Angular 17+?
A: Ні. Модульний код все ще компілюється і працює. CLI генерує standalone за замовчуванням, але можна вибрати інший шлях. Angular-команда планує зробити standalone основним API у майбутній версії, але дати deprecation для NgModules поки немає.

Q: В чому різниця між loadComponent і loadChildren в маршрутизації?
A: loadChildren завантажує модуль або масив standalone-маршрутів. loadComponent завантажує один конкретний компонент як точку входу маршруту. Для feature-областей з кількома вкладеними маршрутами використовують loadChildren з файлом standalone-маршрутів.

Приклади

Реактивна форма в standalone-компоненті

typescript
import { Component, inject } from '@angular/core'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { NgIf } from '@angular/common'; @Component({ selector: 'app-login-form', standalone: true, imports: [ReactiveFormsModule, NgIf], template: ` <form [formGroup]="form" (ngSubmit)="submit()"> <input formControlName="email" type="email" placeholder="Email" /> <span *ngIf="form.get('email')?.touched && form.get('email')?.invalid"> Введіть коректний email </span> <button type="submit" [disabled]="form.invalid">Увійти</button> </form> ` }) export class LoginFormComponent { private fb = inject(FormBuilder); form = this.fb.group({ email: ['', [Validators.required, Validators.email]], }); submit() { if (this.form.valid) { console.log(this.form.value); } } }

ReactiveFormsModule імпортується на рівні компонента. Жодного AppModule, жодного спільного модуля форм десь вище по дереву. Компонент несе свої залежності з собою.

Поєднання standalone з існуючим NgModule

typescript
// Старий feature-модуль, який додає новий standalone-компонент @NgModule({ imports: [ CommonModule, UserCardComponent, // standalone - іде в imports, не в declarations ], declarations: [ LegacyDashboardComponent, // ще не мігрований ], exports: [LegacyDashboardComponent], }) export class LegacyDashboardModule {}

Standalone-компоненти йдуть в imports. Звичайні компоненти залишаються в declarations. Ось як виглядає покрокова міграція без переписування всього дерева модулів одразу.

Standalone-директива в компоненті

typescript
import { Directive, ElementRef, inject, OnInit } from '@angular/core'; @Directive({ selector: '[appAutoFocus]', standalone: true, }) export class AutoFocusDirective implements OnInit { private el = inject(ElementRef); ngOnInit() { this.el.nativeElement.focus(); } } @Component({ selector: 'app-search', standalone: true, imports: [AutoFocusDirective], template: `<input appAutoFocus type="search" placeholder="Пошук..." />` }) export class SearchComponent {}

AutoFocusDirective теж standalone, тому йде прямо в imports компонента. Жодного модуля-посередника. Той самий патерн працює і для standalone-пайпів.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?