Skip to main content

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​

PropertyRole
declarationsRegisters components, directives, and pipes owned by this module
importsImports external modules (Angular built-in, third-party, or custom)
providersServices registered in the DI container (prefer @Injectable({ providedIn: 'root' }))
exportsComponents, directives, and pipes accessible to other modules
bootstrapRoot 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 {}

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​

ItemNgModuleStandalone
Component registrationdeclarations array requiredNot required
Dependency importsManaged at module levelManaged at component level
ReadabilityRequires checking a separate module fileSelf-contained in one component file
Tree shakingPer modulePer component (more granular)
Comparison to ReactNo React equivalentSimilar 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)
HttpClientModuleprovideHttpClient()
BrowserAnimationsModuleprovideAnimationsAsync()
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​

ItemAngular (Standalone)Next.js
Entry pointmain.ts → bootstrapApplicationapp/layout.tsx
Routing configurationapp.routes.ts (code-based)app/ directory structure (file-based)
Configuration fileapp.config.tsnext.config.js
Feature organizationfeatures/ directoryDirectories inside app/
Shared componentsshared/ directorycomponents/ 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() { /* ... */ }
}