NgModule và standalone component khác nhau thế nào?
Câu trả lời phỏng vấn về component thuộc NgModule và standalone component: khai báo, imports, providers, bootstrap, lazy loading và chuyển đổi.
Trả lời ngắn
Angular không có khái niệm chính thức là “module component”. Cách gọi này thường chỉ component được khai báo trong một NgModule. Standalone component không cần nằm trong declarations của NgModule; nó tự khai báo các dependency dùng trong template qua imports. Khác biệt chính là phạm vi biên dịch template và cách tổ chức mã, không phải cách Angular tạo component hay inject service.
Mục lục
- Phân biệt đúng hai khái niệm
- Câu trả lời dùng trong phỏng vấn
- So sánh nhanh
- Cùng một màn hình, hai cách khai báo
- Angular tìm dependency cho template ra sao
providersvà DI có thay đổi không?- Bootstrap và router
- NgModule và standalone có dùng chung được không?
- Khi nào nên chọn cách nào
- Các câu hỏi gài thường gặp
- Lộ trình chuyển từ NgModule sang standalone
- Tóm tắt để nhớ
Phân biệt đúng hai khái niệm
Một component thuộc NgModule là component được đặt trong declarations của một module:
@NgModule({
declarations: [UserCardComponent],
})
export class UsersModule {}Component này không tự mang phạm vi template của nó. Angular xem UsersModule để biết template của UserCardComponent được phép dùng directive, pipe và component nào.
Một standalone component khai báo phạm vi template ngay trong metadata của component:
@Component({
selector: 'app-user-card',
standalone: true,
imports: [DatePipe],
templateUrl: './user-card.component.html',
})
export class UserCardComponent {}Từ Angular 19, standalone mặc định là true với component mới. Khi đọc mã hiện đại, không thấy standalone: true không có nghĩa component đó thuộc NgModule; hãy kiểm tra xem nó có standalone: false hay có nằm trong declarations của module hay không.
Đừng nhầm `NgModule` với ES module
NgModule là class có decorator @NgModule của Angular. Còn import { Component } from '@angular/core' là cú pháp module của TypeScript/JavaScript. Hai chữ “module” này không cùng một khái niệm.
Câu trả lời dùng trong phỏng vấn
Component thuộc
NgModulephải được khai báo trongdeclarations, còn directive, pipe và component mà template của nó dùng được lấy từimportscủaNgModule. Standalone component không cầnNgModule; nó đưa các dependency của template vàoimportscủa chính component. Standalone giúp dependency hiện gần nơi dùng hơn và lazy load component trực tiếp. Nó không thay đổi bản chất của component, template hay DI.NgModulevẫn được Angular hỗ trợ và hai cách có thể cùng tồn tại trong một ứng dụng.
Nếu người phỏng vấn hỏi sâu hơn, nói thêm rằng providers là một vấn đề riêng: vị trí đăng ký provider quyết định scope của service, không phải việc component có standalone hay không.
So sánh nhanh
| Nội dung | Component thuộc NgModule | Standalone component |
|---|---|---|
| Khai báo component | Phải có trong declarations của đúng một NgModule. | Không nằm trong declarations của NgModule. |
| Dependency của template | Lấy từ imports của NgModule chứa component. | Khai báo trong imports của component. |
| Dùng ở nơi khác | Import NgModule đã export component đó. | Import trực tiếp component vào imports của component cha, hoặc import vào NgModule. |
| Bootstrap ứng dụng | Thường là bootstrapModule(AppModule). | Thường là bootstrapApplication(AppComponent, appConfig). |
| Lazy loading | Thường dùng loadChildren để tải module. | Có thể dùng loadComponent để tải trực tiếp page component. |
| Provider dùng chung | Thường cấu hình ở providers của module hoặc providedIn: 'root'. | Thường cấu hình qua ApplicationConfig, route providers, component providers hoặc providedIn: 'root'. |
| Mức độ hỗ trợ hiện tại | Vẫn được hỗ trợ, cần thiết khi làm với mã hoặc thư viện cũ. | Là hướng mặc định cho ứng dụng mới. |
Cùng một màn hình, hai cách khai báo
Ví dụ dưới đây đều tạo một UserCardComponent hiển thị tên và ngày tham gia của người dùng. Điều thay đổi là nơi Angular lấy các dependency cho template.
Cách dựa trên NgModule
UserCardComponent là component không standalone. Nó cần được khai báo trong một module.
// user-card.component.ts
import { Component, Input } from '@angular/core';
export interface User {
name: string;
joinedAt: Date;
}
@Component({
selector: 'app-user-card',
standalone: false,
template: `
<article>
<h2>{{ user.name }}</h2>
<p>Tham gia: {{ user.joinedAt | date: 'dd/MM/yyyy' }}</p>
</article>
`,
})
export class UserCardComponent {
@Input({ required: true }) user!: User;
}Pipe date không được khai báo ở component. UsersModule đưa CommonModule vào phạm vi template của các component mà module khai báo.
// users.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UserCardComponent } from './user-card.component';
@NgModule({
declarations: [UserCardComponent],
imports: [CommonModule],
exports: [UserCardComponent],
})
export class UsersModule {}Một module khác muốn dùng <app-user-card> phải import UsersModule:
@NgModule({
imports: [CommonModule, UsersModule],
})
export class ProfileModule {}Luồng phụ thuộc của template là:
exports là phần dễ quên. declarations chỉ làm component dùng được bên trong UsersModule; muốn module khác dùng nó, module phải export component đó.
Đọc declarations, imports và exports
Ba mảng này trả lời ba câu hỏi khác nhau:
| Mảng | Câu hỏi nó trả lời | Trong ví dụ UsersModule |
|---|---|---|
declarations | Module này sở hữu component, directive và pipe nào? | UserCardComponent thuộc UsersModule. |
imports | Template của các declaration trong module được dùng thêm API UI nào? | CommonModule làm DatePipe, NgIf, NgFor... dùng được. |
exports | Module khác import module này thì được dùng API UI nào? | Module khác dùng được UserCardComponent. |
imports không có nghĩa là chỉ một component trong declarations được dùng CommonModule. Mọi component, directive và pipe trong declarations của UsersModule đều nhận được phạm vi template từ CommonModule.
@NgModule({
declarations: [
UserCardComponent,
UserListPageComponent,
],
imports: [CommonModule],
exports: [UserCardComponent],
})
export class UsersModule {}Trong ví dụ này:
UserCardComponentvàUserListPageComponentđều dùng được*ngIf,*ngFor,datevìUsersModuleimportCommonModule.- Chỉ
UserCardComponentđược module khác dùng, vì chỉ component này có trongexports. UserListPageComponentvẫn dùng bình thường bên trongUsersModule, nhưng module khác không được đặt<app-user-list-page>trong template của họ.
Một component không standalone phải nằm trong declarations của đúng một NgModule. Nếu khai báo cùng một component ở hai module, Angular báo lỗi vì không biết module nào sở hữu compilation scope của component đó.
exports cũng có thể export lại API từ module đã import. Đây là kiểu SharedModule thường gặp trong mã Angular cũ:
@NgModule({
imports: [CommonModule],
exports: [CommonModule],
})
export class SharedModule {}Module import SharedModule sẽ dùng được các API template mà CommonModule export. Cách này tiện khi nhiều module cùng cần một tập directive và pipe, nhưng đừng biến SharedModule thành nơi gom mọi thứ không liên quan.
Cách standalone
Với standalone, UserCardComponent import trực tiếp pipe mà template cần. Không cần UsersModule chỉ để khai báo và export một component.
// user-card.component.ts
import { DatePipe } from '@angular/common';
import { Component, input } from '@angular/core';
export interface User {
name: string;
joinedAt: Date;
}
@Component({
selector: 'app-user-card',
imports: [DatePipe],
template: `
<article>
<h2>{{ user().name }}</h2>
<p>Tham gia: {{ user().joinedAt | date: 'dd/MM/yyyy' }}</p>
</article>
`,
})
export class UserCardComponent {
readonly user = input.required<User>();
}Component cha import trực tiếp UserCardComponent:
import { Component } from '@angular/core';
import { UserCardComponent } from './user-card.component';
@Component({
selector: 'app-profile-page',
imports: [UserCardComponent],
template: `<app-user-card [user]="user" />`,
})
export class ProfilePageComponent {
readonly user = {
name: 'An',
joinedAt: new Date('2025-01-15'),
};
}Luồng phụ thuộc ngắn hơn:
Standalone không có nghĩa mỗi component phải tự import mọi thứ một cách máy móc. Một component có thể import một standalone component khác, hoặc import một NgModule cũ khi cần dùng các declaration mà module đó export.
Angular tìm dependency cho template ra sao
Sự khác nhau cốt lõi nằm ở compilation scope, tức tập directive, pipe và component mà Angular cho phép xuất hiện trong template.
Với component thuộc NgModule, Angular tạo scope từ module:
declarations: các component, directive và pipe mà module sở hữu.imports: các API template mà module nhận từ module khác.exports: các API template mà module cho module khác dùng.
Với standalone component, Angular tạo scope từ imports của component. Mảng này có thể chứa standalone component, standalone directive, standalone pipe hoặc NgModule.
Ví dụ, hai đoạn sau đều hợp lệ nhưng thuộc hai cơ chế khác nhau:
// NgModule: NgIf đến từ CommonModule mà module imports.
@NgModule({
declarations: [OrdersPageComponent],
imports: [CommonModule],
})
export class OrdersModule {}// Standalone: NgIf đến từ imports của chính component.
@Component({
imports: [NgIf],
template: `<p *ngIf="isLoading">Đang tải...</p>`,
})
export class OrdersPageComponent {
isLoading = true;
}Nếu quên NgIf trong imports của standalone component, Angular không biết *ngIf là directive nào. Lỗi này tương đương với việc quên import CommonModule vào NgModule trong kiến trúc cũ.
providers và DI có thay đổi không?
Không. Standalone thay đổi cách ghép dependency cho template, còn DI vẫn dùng token, provider và injector như nhau.
@Injectable({ providedIn: 'root' })
export class UserApi {
// Một instance cho application injector.
}Service trên dùng được trong cả component thuộc NgModule lẫn standalone component:
@Component({
selector: 'app-user-card',
template: `...`,
})
export class UserCardComponent {
private readonly userApi = inject(UserApi);
}Các scope provider cần nhớ:
| Vị trí đăng ký | Hệ quả |
|---|---|
providedIn: 'root' hoặc application config | Thường có một instance trong toàn ứng dụng. |
providers của route | Instance sống theo route đang hoạt động. |
providers của component | Component đó và cây con có instance riêng. |
providers của NgModule | Có scope theo injector nơi module được nạp; cần đọc kỹ với eager và lazy module. |
Câu trả lời dễ bị hỏi tiếp
Standalone component không tự làm service thành singleton. Service là singleton hay không phụ thuộc vào provider được đăng ký ở root, route hay component.
Bootstrap và router
Đây là hai việc xảy ra nối tiếp nhau, nhưng không phải một việc:
- Bootstrap tạo ứng dụng Angular và render root component, thường là
AppComponent. - Router đọc URL hiện tại, chọn route phù hợp rồi render page vào
<router-outlet>bên trong root component.
Bootstrap chỉ khởi động ứng dụng
Trong kiến trúc NgModule, Angular khởi động từ root module. Module này nói Angular component nào là root component qua mảng bootstrap.
// main.ts
platformBrowserDynamic().bootstrapModule(AppModule);// app.module.ts
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
bootstrap: [AppComponent],
})
export class AppModule {}Ý nghĩa của đoạn trên là: Angular tạo AppModule, đọc bootstrap: [AppComponent], rồi tạo AppComponent và gắn nó vào phần tử <app-root> trong index.html.
Với standalone, không cần AppModule làm bước trung gian. Angular nhận trực tiếp root component:
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(),
],
});bootstrapApplication(AppComponent, ...) cũng tạo application injector. Mảng providers ở đây là nơi đăng ký các service dùng cho toàn app, chẳng hạn Router và HttpClient.
RouterOutlet và provideRouter làm hai việc khác nhau
Đây là cặp API rất dễ nhầm:
| API | Vai trò | Đặt ở đâu trong standalone app? |
|---|---|---|
RouterOutlet | Directive tạo vị trí trong template để Router render page hiện tại. | imports của AppComponent, vì nó được dùng trong template. |
provideRouter(routes) | Đăng ký Router và cấu hình route vào DI của application. | providers khi gọi bootstrapApplication(). |
Root component standalone thường có dạng sau:
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
template: `<router-outlet />`,
})
export class AppComponent {}Nếu chỉ có provideRouter(routes) mà AppComponent không có <router-outlet>, Router vẫn biết URL nhưng không có chỗ trong giao diện để đặt page. Ngược lại, chỉ thêm <router-outlet> mà không gọi provideRouter(routes) thì Angular không có Router service và cấu hình route.
Trong app dựa trên NgModule, hai việc này thường bị che bởi AppRoutingModule:
// app-routing.module.ts
const routes: Routes = [
{ path: 'users', loadChildren: () => import('./users/users.module').then((m) => m.UsersModule) },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}RouterModule.forRoot(routes) đăng ký Router. exports: [RouterModule] làm router-outlet, routerLink và các directive router khác dùng được trong template của AppModule.
Router tải trang theo URL
Trong standalone app, route có thể tải một page component trực tiếp:
// app.routes.ts
export const routes: Routes = [
{
path: 'users',
loadComponent: () =>
import('./users-page.component').then((m) => m.UsersPageComponent),
},
];Khi người dùng vào /users, Router mới chạy import(...), lấy UsersPageComponent, tạo component đó và đặt nó vào <router-outlet>. Đây là lazy loading: mã của page không cần nằm trong JavaScript tải ban đầu.
loadChildren vẫn dùng được. Trong mã cũ, nó thường tải một lazy NgModule:
{
path: 'admin',
loadChildren: () =>
import('./admin/admin.module').then((m) => m.AdminModule),
}Trong mã standalone hiện đại, loadChildren cũng có thể tải một mảng routes của cả feature. Dùng loadComponent khi route chỉ cần một page; dùng loadChildren khi feature có nhiều route con như /admin/users, /admin/roles và /admin/settings.
NgModule và standalone có dùng chung được không?
Có. Đây là điều quan trọng khi nâng cấp ứng dụng cũ.
- Một standalone component có thể import
NgModuleđể dùng các component, directive và pipe mà module export. - Một
NgModulecó thể import standalone component, directive hoặc pipe. - Router có thể có route tải module cũ bằng
loadChildrenvà route tải standalone page bằngloadComponenttrong cùng một ứng dụng. importProvidersFrom()cho phép đưa provider của module cũ vào application config khi cần tương thích.
Ví dụ module cũ dùng standalone component:
@NgModule({
imports: [CommonModule, UserCardComponent],
declarations: [LegacyProfilePageComponent],
})
export class LegacyProfileModule {}Vì thế không cần dừng toàn bộ dự án để “chuyển sang standalone”. Có thể chuyển từng feature hoặc từng route.
Khi nào nên chọn cách nào
Ứng dụng mới
Chọn standalone làm mặc định. Angular hiện đại tạo component mới theo hướng này, bootstrap và route cũng có API trực tiếp. Mã ít lớp trung gian hơn, đặc biệt với feature nhỏ và lazy route.
Ứng dụng đang dùng NgModule
Không cần đổi chỉ vì standalone mới hơn. Hãy ưu tiên chuyển khi đang sửa feature, cần giảm độ phức tạp của shared module, hoặc muốn dùng cách bootstrap/router mới. Nếu ứng dụng ổn định và module boundaries rõ ràng, việc chuyển đổi có thể làm dần.
Thư viện hoặc dependency cũ
Bạn vẫn có thể gặp thư viện chỉ export NgModule. Standalone component import module đó bình thường. Không cần tạo wrapper module trừ khi muốn gom một tập dependency UI được dùng lặp lại.
Các câu hỏi gài thường gặp
Standalone có thay thế hoàn toàn NgModule không?
Không. Standalone là cách được ưu tiên cho mã mới, nhưng NgModule vẫn được Angular hỗ trợ. Nó còn xuất hiện nhiều trong dự án cũ và thư viện.
Standalone có phải là component không có module TypeScript không?
Không. Mọi file TypeScript vẫn dùng import/export bình thường. “Standalone” chỉ nói component không phụ thuộc vào NgModule của Angular để tham gia compilation scope.
Có phải standalone component không được dùng trong NgModule?
Không. NgModule có thể đặt standalone component trong imports. Không đặt nó trong declarations.
Có cần thêm CommonModule vào mọi standalone component không?
Không bắt buộc. Bạn có thể import trực tiếp NgIf, NgFor, AsyncPipe, DatePipe hoặc API standalone mà template cần. Import CommonModule vẫn hợp lệ khi dùng nhiều directive/pipe phổ biến.
imports của standalone component có phải là DI provider không?
Không. imports tạo phạm vi cho template. Provider được đặt ở providers, providedIn, application config hoặc route config.
Standalone có làm bundle nhỏ hơn ngay lập tức không?
Không thể kết luận chỉ từ chữ standalone. Bundle còn phụ thuộc vào route splitting, dependency thực tế, tree-shaking và cấu hình build. Lợi ích rõ hơn là dependency của template dễ nhìn và lazy-load component trực tiếp hơn.
Lộ trình chuyển từ NgModule sang standalone
Chuyển theo feature để thay đổi nhỏ và dễ kiểm tra.
- Nâng Angular và các thư viện lên phiên bản hỗ trợ standalone.
- Chuyển leaf component trước: thêm
standalone: true, bỏ component khỏideclarations, rồi đưa các dependency template vàoimports. - Cập nhật component/module đang dùng nó: standalone component đi vào
imports, không đi vàodeclarations. - Chuyển một route sang
loadComponentnếu route đó chỉ tải một page. - Chuyển bootstrap sang
bootstrapApplication()sau khi các provider cấp ứng dụng đã được xác định rõ. - Chạy test và kiểm tra đặc biệt các provider trước đây được đăng ký trong module lazy.
Điểm dễ tạo lỗi khi migration
Đừng chuyển providers từ lazy NgModule lên application config theo thói quen. Việc này có thể biến service theo feature thành singleton toàn ứng dụng, làm state không được reset khi rời route.
Tóm tắt để nhớ
- Component thuộc
NgModule: khai báo ởdeclarations; template nhận dependency quaimportscủa module. - Standalone component: không cần
declarations; template nhận dependency quaimportscủa component. importsphục vụ template;providersphục vụ DI. Hai mảng không thay thế nhau.- Standalone không bỏ
NgModule, không thay đổi component lifecycle và không tự thay đổi scope của service. - Với app mới, dùng standalone làm mặc định. Với app cũ, chuyển dần theo feature và giữ nguyên những module đang hoạt động tốt.
Kỹ thuật debugging Angular
Quy trình tìm nguyên nhân lỗi Angular bằng tái hiện, DevTools, source map, Network và kiểm tra state/template.
Signals vs RxJS: Signals có thay thế được RxJS không?
Câu trả lời phỏng vấn về Signals và RxJS trong Angular: khác biệt giữa state và stream, phần có thể thay thế, phần nên giữ RxJS và ví dụ kết hợp thực tế.