Standalone Components and Modules
Standalone components introduced in Angular 14 enable development without traditional NgModules. This document covers the recommended modern Angular approach.
1. NgModule (Traditional Approach)
React and Next.js have no concept equivalent to Angular's NgModule. NgModule was the primary building block of Angular from version 2 through 13.
@NgModule Decorator
NgModule is defined using the @NgModule decorator.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserListComponent } from './user-list/user-list.component';
import { UserService } from './user.service';
@NgModule({
declarations: [
AppComponent, // Components, directives, and pipes owned by this module
UserListComponent,
],
imports: [
BrowserModule, // Import other modules
FormsModule,
AppRoutingModule,
],
providers: [
UserService, // DI providers (recommended: @Injectable({ providedIn: 'root' }))
],
exports: [
UserListComponent, // Components made available to other modules
],
bootstrap: [AppComponent], // Root module only: the root component to bootstrap
})
export class AppModule {}
Role of Each Property
| Property | Role |
|---|---|
declarations | Registers components, directives, and pipes owned by this module |
imports | Imports external modules (Angular built-in, third-party, or custom) |
providers | Services registered in the DI container (prefer @Injectable({ providedIn: 'root' })) |
exports | Components, directives, and pipes accessible to other modules |
bootstrap | Root component to launch (root module only) |
Why NgModule Was Required
In earlier Angular, every component had to be registered in some NgModule's declarations array before it could be used. NgModule acted as the compilation context boundary, defining which components, directives, and pipes were available.
// Angular 13 and earlier: UserCardComponent must be added to AppModule's declarations
@NgModule({
declarations: [AppComponent, UserCardComponent], // All components listed here
...
})
export class AppModule {}
2. Standalone Components (Recommended)
Introduced in Angular 14 and the default since Angular 17, standalone components work without NgModules.
Specifying standalone: true
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { UserCardComponent } from './user-card.component';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [
CommonModule, // Directives like *ngIf, *ngFor
RouterLink, // Router link directive
UserCardComponent, // Directly import other standalone components
],
template: `
<div *ngFor="let user of users">
<app-user-card [user]="user" />
</div>
<a routerLink="/home">Go Home</a>
`,
})
export class UserListComponent {
users = [{ name: 'Alice' }, { name: 'Bob' }];
}
NgModule vs. Standalone Comparison
| Item | NgModule | Standalone |
|---|---|---|
| Component registration | declarations array required | Not required |
| Dependency imports | Managed at module level | Managed at component level |
| Readability | Requires checking a separate module file | Self-contained in one component file |
| Tree shaking | Per module | Per component (more granular) |
| Comparison to React | No React equivalent | Similar to React's import statements |
Generating a Project with ng new --standalone
From Angular 17 onwards, ng new generates a standalone project by default.
# Angular 17+: standalone by default
ng new my-app
# Explicitly specify standalone
ng new my-app --standalone
# Use the legacy NgModule-based approach (not recommended)
ng new my-app --no-standalone
Difference in generated files:
// Standalone (new): app.component.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent {}
// NgModule-based (legacy): app.module.ts exists
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
bootstrap: [AppComponent],
})
export class AppModule {}
3. bootstrapApplication
With standalone components, the application is launched using the bootstrapApplication function instead of AppModule.
Bootstrapping the Application in main.ts
// main.ts (standalone approach)
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
],
};
Functional Providers
Modern Angular uses functional providers instead of modules.
| Legacy (NgModule-based) | Modern (Functional Provider) |
|---|---|
RouterModule.forRoot(routes) | provideRouter(routes) |
HttpClientModule | provideHttpClient() |
BrowserAnimationsModule | provideAnimationsAsync() |
StoreModule.forRoot(reducers) | provideStore(reducers) |
EffectsModule.forRoot(effects) | provideEffects(effects) |
// app.config.ts (detailed configuration example)
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { authInterceptor } from './core/interceptors/auth.interceptor';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withPreloading(PreloadAllModules)),
provideHttpClient(withInterceptors([authInterceptor])),
provideAnimationsAsync(),
],
};
4. Migrating from NgModule to Standalone
This section explains how to migrate an existing NgModule-based project to standalone components.
Automatic Migration with Angular CLI
Angular CLI provides an automatic migration command.
# Run the migration schematic
ng generate @angular/core:standalone
# A selection prompt appears:
# 1. convert-to-standalone: Convert components to standalone
# 2. prune-ng-modules: Remove NgModules that are no longer needed
# 3. standalone-bootstrap: Convert to bootstrapApplication
Manual Migration Steps
Step 1: Add standalone: true to components
// Before (NgModule-based)
@Component({
selector: 'app-user-card',
template: `<div>{{ user.name }}</div>`,
})
export class UserCardComponent {}
// After (Standalone)
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule],
template: `<div>{{ user.name }}</div>`,
})
export class UserCardComponent {}
Step 2: Remove the component from NgModule's declarations and move it to imports
@NgModule({
declarations: [
AppComponent,
// UserCardComponent removed (now standalone)
],
imports: [
BrowserModule,
UserCardComponent, // Moved here
],
})
export class AppModule {}
Step 3: Update dependency imports
// Standalone component directly imports other standalone components
@Component({
standalone: true,
imports: [
CommonModule,
UserCardComponent, // Directly import standalone component
SharedButtonComponent,
],
})
export class UserListComponent {}
Hybrid Approach
During the migration period, NgModules and standalone components can coexist.
// Standalone components can be imported inside NgModules
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StandaloneButtonComponent, // Add standalone component to imports
],
})
export class AppModule {}
// Standalone components can import NgModules
@Component({
standalone: true,
imports: [
LegacyFeatureModule, // Legacy NgModule can be imported
],
})
export class NewFeatureComponent {}
5. Project Structure Best Practices
Feature-based Directory Structure
Recommended modern Angular project structure using standalone components:
src/
├── app/
│ ├── core/ # Singleton services, guards, and interceptors
│ │ ├── guards/
│ │ │ └── auth.guard.ts
│ │ ├── interceptors/
│ │ │ └── auth.interceptor.ts
│ │ └── services/
│ │ └── auth.service.ts
│ ├── shared/ # Shared components, directives, and pipes
│ │ ├── components/
│ │ │ └── button/
│ │ │ ├── button.component.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── features/ # Feature-specific directories
│ │ ├── dashboard/
│ │ │ ├── components/
│ │ │ ├── pages/
│ │ │ │ └── dashboard.component.ts
│ │ │ ├── services/
│ │ │ └── index.ts
│ │ └── user/
│ │ ├── components/
│ │ │ └── user-card.component.ts
│ │ ├── pages/
│ │ │ └── user-list.component.ts
│ │ ├── services/
│ │ │ └── user.service.ts
│ │ └── index.ts
│ ├── app.component.ts
│ ├── app.config.ts # bootstrapApplication configuration
│ └── app.routes.ts # Route definitions
└── main.ts
Comparison with Next.js app/ Directory Structure
| Item | Angular (Standalone) | Next.js |
|---|---|---|
| Entry point | main.ts → bootstrapApplication | app/layout.tsx |
| Routing configuration | app.routes.ts (code-based) | app/ directory structure (file-based) |
| Configuration file | app.config.ts | next.config.js |
| Feature organization | features/ directory | Directories inside app/ |
| Shared components | shared/ directory | components/ directory |
Managing Shared Components
// shared/components/button/button.component.ts
@Component({
selector: 'app-button',
standalone: true,
template: `
<button [class]="variant" (click)="clicked.emit()">
<ng-content />
</button>
`,
})
export class ButtonComponent {
@Input() variant: 'primary' | 'secondary' = 'primary';
@Output() clicked = new EventEmitter<void>();
}
Barrel Exports (index.ts)
Barrel exports keep import paths concise.
// shared/index.ts
export { ButtonComponent } from './components/button/button.component';
export { InputComponent } from './components/input/input.component';
export { ModalComponent } from './components/modal/modal.component';
// ❌ Without barrel exports (verbose paths)
import { ButtonComponent } from '../../../shared/components/button/button.component';
import { InputComponent } from '../../../shared/components/input/input.component';
// ✅ With barrel exports (clean imports)
import { ButtonComponent, InputComponent } from '../../../shared';
// features/user/pages/user-list.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonComponent, InputComponent } from '../../../shared';
import { UserCardComponent } from '../components/user-card.component';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule, ButtonComponent, InputComponent, UserCardComponent],
template: `
<div class="user-list">
<app-input placeholder="Search..." (valueChange)="search($event)" />
<app-user-card *ngFor="let user of users" [user]="user" />
<app-button variant="primary" (clicked)="loadMore()">Load More</app-button>
</div>
`,
})
export class UserListComponent {
users = this.userService.getUsers();
constructor(private userService: UserService) {}
search(query: string) { /* ... */ }
loadMore() { /* ... */ }
}