Окремі компоненти в 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так само, як компоненти
Швидкий приклад
@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
// 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 модуля.
Ліниве завантаження одного компонента
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
// Не скомпілюється
@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-компоненті
// Неправильно - declarations не існує в standalone
@Component({
standalone: true,
declarations: [ChildComponent], // цієї властивості тут немає
})
export class ParentComponent {}Standalone-компоненти використовують imports для всього. declarations - це концепція модуля.
Занадто широкий імпорт
// Компілюється, але тягне весь 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-компоненті
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
// Старий feature-модуль, який додає новий standalone-компонент
@NgModule({
imports: [
CommonModule,
UserCardComponent, // standalone - іде в imports, не в declarations
],
declarations: [
LegacyDashboardComponent, // ще не мігрований
],
exports: [LegacyDashboardComponent],
})
export class LegacyDashboardModule {}Standalone-компоненти йдуть в imports. Звичайні компоненти залишаються в declarations. Ось як виглядає покрокова міграція без переписування всього дерева модулів одразу.
Standalone-директива в компоненті
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-пайпів.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.