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.tsbootstrapApplicationapp/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() { /* ... */ }
}