Standalone Components: Angular Without NgModules
With the introduction of Standalone Components in Angular 14 and their progressive adoption as the standard starting from Angular 15, the framework underwent a fundamental architectural shift: removing the requirement to use NgModules for code organization. This evolution drastically simplified development, reduced boilerplate, and made Angular far more accessible to newcomers.
In this fourth article of the Modern Angular series, we will explore Standalone
Components in depth: from the problem they solve to complete migration, covering routing,
bootstrapping, dependency injection, and advanced patterns. If you have ever struggled with
declarations, imports, and exports in Angular modules,
this article will show you how to leave them behind for good.
What You Will Learn
- Why NgModules were a problem and how Standalone Components solve it
- How to create standalone components, directives, and pipes
- Managing dependencies with
importsdirectly in the component - Configuring routing and lazy loading without modules
- Bootstrapping the application with
bootstrapApplication() - Dependency Injection in the standalone world
- Migrating an existing project with the official schematic
- Advanced patterns and best practices for enterprise applications
Modern Angular Series Overview
| # | Article | Topic |
|---|---|---|
| 1 | Angular Signals | Fine-grained reactivity |
| 2 | Zoneless Change Detection | Removing Zone.js |
| 3 | New Template Syntax: @if, @for, @defer | Control flow blocks |
| 4 | You are here - Standalone Components | Module-free architecture |
| 5 | Signal Forms | Reactive forms with Signals |
| 6 | Angular SSR and Incremental Hydration | Server-side rendering |
| 7 | Core Web Vitals in Angular | Performance optimization |
| 8 | Angular PWA | Progressive Web Apps |
| 9 | Advanced Dependency Injection | DI patterns and tokens |
| 10 | Migrating from Angular 17 to 21 | Step-by-step migration |
1. The NgModule Problem
Since version 2, Angular has used NgModules as the fundamental unit for code
organization. Every component, directive, or pipe had to be declared in exactly one module via
the declarations array, and inter-module dependencies were managed through
imports and exports. While this system provided clear structure,
it generated increasingly severe problems as applications grew in complexity.
// app.module.ts - The traditional approach
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';
import { HomeComponent } from './home/home.component';
import { ProductListComponent } from './products/product-list.component';
import { ProductDetailComponent } from './products/product-detail.component';
import { CartComponent } from './cart/cart.component';
import { CheckoutComponent } from './checkout/checkout.component';
import { HighlightDirective } from './directives/highlight.directive';
import { CurrencyPipe } from './pipes/currency.pipe';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
FooterComponent,
HomeComponent,
ProductListComponent,
ProductDetailComponent,
CartComponent,
CheckoutComponent,
HighlightDirective,
CurrencyPipe
],
imports: [
BrowserModule,
HttpClientModule,
ReactiveFormsModule,
RouterModule.forRoot(routes)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Common Problems with NgModules
- Excessive boilerplate: Every component requires import and declaration in a module. Large projects can have hundreds of lines just for declarations
- Circular dependencies: When two modules import each other, hard-to-diagnose errors arise
- Declarations vs imports confusion: New developers constantly confuse where to put components (declarations) and where to put modules (imports)
- "Component is not part of any NgModule" error: One of the most frustrating errors for beginners, caused by forgetting to declare a component
- Tight coupling: Components are coupled to the module that declares them, making reuse and unit testing more difficult
- Complex lazy loading: To lazy load a feature, you needed to create a dedicated module with its own routing configuration
Consider a typical circular dependency example that plagued many Angular projects:
// shared.module.ts
@NgModule({
declarations: [UserAvatarComponent],
imports: [ProductModule], // Imports ProductModule
exports: [UserAvatarComponent]
})
export class SharedModule { }
// product.module.ts
@NgModule({
declarations: [ProductCardComponent],
imports: [SharedModule], // Imports SharedModule → CIRCULAR!
exports: [ProductCardComponent]
})
export class ProductModule { }
NgModules vs Standalone: Quick Comparison
| Aspect | NgModules | Standalone |
|---|---|---|
| Component declaration | In module's declarations |
Self-contained with standalone: true |
| Dependency management | Through intermediate modules | Directly in the component |
| Lazy loading | Requires dedicated NgModule | Direct loadComponent() |
| Boilerplate | High (module + declarations) | Minimal |
| Unit testing | TestBed with test module setup | Minimal configuration |
| Circular dependencies | Frequent between modules | Nearly impossible |
| Learning curve | Steep (declarations, imports, exports, providers, bootstrap) | Gentle (standalone: true, imports) |
2. What Are Standalone Components
A Standalone Component is an Angular component that declares itself as
self-sufficient through the standalone: true flag in the
@Component decorator. This means the component does not need to be declared
in any NgModule: it manages its own dependencies directly through its imports array.
Starting with Angular 19, all components, directives, and pipes are standalone
by default. You do not even need to specify standalone: true
because it is the default behavior. This is the definitive signal that the Angular team considers
Standalone Components the future of the framework.
// Angular 14-18: explicit standalone flag
@Component({
selector: 'app-user-card',
standalone: true,
template: `<div class="user-card">
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
</div>`,
styles: [`
.user-card {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
}
`]
})
export class UserCardComponent {
user = input.required<User>();
}
// Angular 19+: standalone is the default, no need to specify it
@Component({
selector: 'app-user-card',
template: `<div class="user-card">
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
</div>`,
styles: [`
.user-card {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
}
`]
})
export class UserCardComponent {
user = input.required<User>();
}
Note on standalone: true in Angular 19+
From Angular 19 onward, if you omit the standalone property, the component is
standalone by default. If you explicitly want to create a non-standalone component (for
compatibility with legacy code), you must specify standalone: false.
Throughout this article we will use standalone: true for clarity,
but remember that in Angular 21 it is optional.
3. Creating Standalone Components, Directives, and Pipes
The "standalone" concept does not apply only to components. Directives and pipes can also be standalone, following the same principle of self-sufficiency.
Standalone Component
import { Component, input, output, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CommonModule], // Dependencies managed here
template: `
<div class="todo-item" [class.completed]="todo().completed">
<input type="checkbox"
[checked]="todo().completed"
(change)="onToggle()" />
<span>{{ todo().title }}</span>
<button (click)="onDelete()">Delete</button>
</div>
`
})
export class TodoItemComponent {
todo = input.required<Todo>();
toggled = output<number>();
deleted = output<number>();
onToggle() {
this.toggled.emit(this.todo().id);
}
onDelete() {
this.deleted.emit(this.todo().id);
}
}
Standalone Directive
import { Directive, ElementRef, input, effect } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
appHighlight = input<string>('yellow');
constructor(private el: ElementRef) {
effect(() => {
this.el.nativeElement.style.backgroundColor = this.appHighlight();
});
}
}
Standalone Pipe
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'timeAgo',
standalone: true
})
export class TimeAgoPipe implements PipeTransform {
transform(value: Date): string {
const seconds = Math.floor((new Date().getTime() - value.getTime()) / 1000);
if (seconds < 60) return 'just now';
if (seconds < 3600) return Math.floor(seconds / 60) + ' minutes ago';
if (seconds < 86400) return Math.floor(seconds / 3600) + ' hours ago';
return Math.floor(seconds / 86400) + ' days ago';
}
}
4. Managing Dependencies with imports
The key to Standalone Components is the imports array in the
@Component decorator. Here you declare all template dependencies:
other components, directives, pipes, and even entire NgModules (for compatibility with legacy
libraries). Each component knows exactly what it needs, with no intermediaries.
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { AsyncPipe, DatePipe, CurrencyPipe } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
// Import standalone components directly
import { UserCardComponent } from './user-card.component';
import { LoadingSpinnerComponent } from '../shared/loading-spinner.component';
import { HighlightDirective } from '../directives/highlight.directive';
import { TimeAgoPipe } from '../pipes/time-ago.pipe';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
// Angular modules (for compatibility)
ReactiveFormsModule,
// Standalone pipes
AsyncPipe,
DatePipe,
CurrencyPipe,
TimeAgoPipe,
// Standalone components
UserCardComponent,
LoadingSpinnerComponent,
// Standalone directives
HighlightDirective,
// Router
RouterLink,
RouterOutlet
],
templateUrl: './dashboard.component.html'
})
export class DashboardComponent {
// The component knows all its dependencies
}
Comparison: Dependencies with NgModules vs Standalone
| With NgModules | With Standalone |
|---|---|
| Component declared in a module | Component is self-sufficient |
| Dependencies imported in the module | Dependencies imported in the component |
| All module declarations see the imports | Only the component sees its imports |
| Risk of unused imports in the module | Every import is explicit and necessary |
Warning: Do Not Import More Than Needed
With Standalone Components you might be tempted to import the entire CommonModule
for convenience. However, it is better to import only the individual pipes and directives you
need (e.g., NgIf, NgFor, AsyncPipe). With the new
@if and @for syntax from Angular 17+, many of these imports
are no longer necessary because control flow is built into the template natively.
5. Routing Without NgModules
One of the biggest advantages of Standalone Components is the simplification of
routing. With modules, lazy loading a feature required creating a dedicated module
with RouterModule.forChild(). With Standalone Components, you can lazy load a
single component directly using loadComponent.
loadComponent: Lazy Loading a Single Component
// app.routes.ts - Complete routing configuration without modules
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'products',
loadComponent: () =>
import('./products/product-list.component').then(m => m.ProductListComponent)
},
{
path: 'products/:id',
loadComponent: () =>
import('./products/product-detail.component').then(m => m.ProductDetailComponent)
},
{
path: 'cart',
loadComponent: () =>
import('./cart/cart.component').then(m => m.CartComponent)
},
{
path: '**',
loadComponent: () =>
import('./not-found/not-found.component').then(m => m.NotFoundComponent)
}
];
loadChildren: Lazy Loading a Group of Routes
For complex features with sub-routes, you can use loadChildren pointing to a file
that exports a Routes array, with no need for a module wrapper.
// app.routes.ts
export const routes: Routes = [
{
path: 'admin',
loadChildren: () =>
import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
}
];
// admin/admin.routes.ts - Standalone routes file
import { Routes } from '@angular/router';
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./admin-layout.component').then(m => m.AdminLayoutComponent),
children: [
{
path: 'dashboard',
loadComponent: () =>
import('./dashboard/dashboard.component').then(m => m.DashboardComponent)
},
{
path: 'users',
loadComponent: () =>
import('./users/users.component').then(m => m.UsersComponent)
},
{
path: 'settings',
loadComponent: () =>
import('./settings/settings.component').then(m => m.SettingsComponent)
}
]
}
];
Lazy Loading: NgModules vs Standalone
| Approach | NgModules | Standalone |
|---|---|---|
| Lazy load single component | Impossible without module wrapper | loadComponent: () => import(...) |
| Lazy load feature | loadChildren with NgModule |
loadChildren with Routes array |
| Files required | feature.module.ts + feature-routing.module.ts | feature.routes.ts (only) |
| Granularity | Module level | Individual component level |
6. Bootstrapping with bootstrapApplication()
With Standalone Components, the way you bootstrap the application also changes radically.
You no longer need an AppModule with the platformBrowserDynamic().bootstrapModule()
method. Instead, you use bootstrapApplication(), which accepts the root component
and a provider configuration.
The Old Approach with NgModule
// main.ts - Traditional approach
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
The New Standalone Approach
// main.ts - Standalone approach
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig)
.catch(err => console.error(err));
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withViewTransitions } from '@angular/router';
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
// Change detection (or provideExperimentalZonelessChangeDetection())
provideZoneChangeDetection({ eventCoalescing: true }),
// Router with view transitions
provideRouter(routes, withViewTransitions()),
// HTTP client with functional interceptors
provideHttpClient(
withFetch(),
withInterceptors([authInterceptor, errorInterceptor])
),
// Async animations
provideAnimationsAsync(),
// Hydration for SSR
provideClientHydration(withEventReplay())
]
};
The provide* Functions
With the standalone approach, Angular introduces provide* functions that replace the old configuration modules. Here are the main ones:
| provide* Function | Replaces |
|---|---|
provideRouter() |
RouterModule.forRoot() |
provideHttpClient() |
HttpClientModule |
provideAnimationsAsync() |
BrowserAnimationsModule |
provideClientHydration() |
SSR configuration in module |
provideZoneChangeDetection() |
Implicit Zone.js configuration |
7. Dependency Injection in the Standalone World
Angular's Dependency Injection (DI) works perfectly with Standalone Components, but offers new patterns for configuring providers at different levels of the application.
providedIn: 'root' - The Recommended Pattern
The simplest and recommended way to provide a service is to use
providedIn: 'root' in the @Injectable decorator.
This registers the service as a singleton in the root injector, available everywhere in the
application. The advantage is that the service is tree-shakable: if no component injects it,
it gets removed from the production bundle.
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root' // Tree-shakable singleton
})
export class CartService {
private _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();
readonly totalItems = computed(() => this._items().length);
readonly totalPrice = computed(() =>
this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
addItem(product: Product) {
this._items.update(items => [...items, {
...product,
quantity: 1
}]);
}
removeItem(id: number) {
this._items.update(items => items.filter(item => item.id !== id));
}
}
Route-Level Providers
For services that should only be shared within a feature, you can define providers directly in the route configuration. This creates a dedicated environment injector for that section of the application.
// admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
{
path: '',
providers: [
// This service is available ONLY in admin components
AdminAuthService,
// Configurable providers also work
{
provide: API_BASE_URL,
useValue: '/api/admin'
}
],
children: [
{
path: 'dashboard',
loadComponent: () =>
import('./dashboard.component').then(m => m.DashboardComponent)
},
{
path: 'users',
loadComponent: () =>
import('./users.component').then(m => m.UsersComponent)
}
]
}
];
Component-Level Providers
@Component({
selector: 'app-editor',
standalone: true,
providers: [
// New instance for every editor component
EditorStateService,
{
provide: UNDO_STACK_SIZE,
useValue: 50
}
],
template: `...`
})
export class EditorComponent {
private editorState = inject(EditorStateService);
}
Provider Hierarchy in the Standalone World
| Level | Configuration | Scope | Use Case |
|---|---|---|---|
| Root | providedIn: 'root' |
Entire app (singleton) | AuthService, HttpService, StateStore |
| App Config | appConfig.providers |
Entire app | Router, HttpClient, Animations |
| Route | route.providers |
Feature/section | AdminService, FeatureConfig |
| Component | component.providers |
Single component | FormState, EditorState |
8. Migrating from NgModules to Standalone
Angular provides an official migration schematic that automates most of the conversion process from NgModules to Standalone Components. The migration occurs in three progressive phases, allowing you to proceed safely and incrementally.
Phase 1: Convert Components to Standalone
The first step converts all components, directives, and pipes to standalone by adding
standalone: true and the necessary imports.
# Phase 1: Convert components, directives, and pipes to standalone
ng generate @angular/core:standalone-migration --mode=convert-to-standalone
# What it does:
# - Adds standalone: true to all components/directives/pipes
# - Adds the necessary imports in each component's decorator
# - Keeps existing NgModules for compatibility
Phase 2: Remove Unnecessary NgModules
# Phase 2: Remove NgModules that are no longer needed
ng generate @angular/core:standalone-migration --mode=prune-ng-modules
# What it does:
# - Removes NgModules that contain only standalone declarations
# - Updates imports in files that referenced them
# - Keeps modules that define providers or other configurations
Phase 3: Switch to bootstrapApplication()
# Phase 3: Convert the application bootstrap
ng generate @angular/core:standalone-migration --mode=standalone-bootstrap
# What it does:
# - Converts main.ts to use bootstrapApplication()
# - Creates app.config.ts with providers
# - Removes AppModule if no longer necessary
# - Migrates providers from the module to the configuration
Tips for a Safe Migration
- Commit before each phase: This way you can easily roll back if something goes wrong
- Run tests after each phase: Make sure the application works correctly before proceeding
- Check for circular dependencies: The migration may reveal hidden circular dependencies in modules
- Review providers: Some modules define providers with
forRoot()andforChild()that require manual attention - Third-party libraries: Verify that the libraries you use support standalone. Most do by exporting standalone components or providing
provide*()functions
Manual Step-by-Step Migration
If you prefer to migrate manually, here is the typical process for converting a component:
// BEFORE: product-card.component.ts (in an NgModule)
@Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
@Input() product!: Product;
@Output() addToCart = new EventEmitter<Product>();
}
// product.module.ts
@NgModule({
declarations: [ProductCardComponent, ProductListComponent],
imports: [CommonModule, RouterModule],
exports: [ProductCardComponent]
})
export class ProductModule { }
// AFTER: product-card.component.ts (standalone)
import { Component, input, output } from '@angular/core';
import { RouterLink } from '@angular/router';
import { CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-product-card',
standalone: true,
imports: [RouterLink, CurrencyPipe],
templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
product = input.required<Product>();
addToCart = output<Product>();
}
// product.module.ts → DELETED!
// Routes point directly to the components
9. Advanced Patterns
Barrel Exports for Shared Components
When you have many shared standalone components, it is useful to create a barrel file
(index.ts) that re-exports them all. This simplifies imports in the components
that use them.
// shared/ui/index.ts - Barrel export
export { ButtonComponent } from './button/button.component';
export { CardComponent } from './card/card.component';
export { ModalComponent } from './modal/modal.component';
export { TabsComponent } from './tabs/tabs.component';
export { TooltipDirective } from './tooltip/tooltip.directive';
export { TimeAgoPipe } from './pipes/time-ago.pipe';
export { TruncatePipe } from './pipes/truncate.pipe';
// Usage in a component:
import {
ButtonComponent,
CardComponent,
TooltipDirective,
TruncatePipe
} from '../shared/ui';
@Component({
standalone: true,
imports: [ButtonComponent, CardComponent, TooltipDirective, TruncatePipe],
// ...
})
export class MyFeatureComponent { }
Composing Common Import Arrays
If multiple components share the same dependencies, you can create a common imports array to reuse. This is a lightweight pattern that replaces SharedModules.
// shared/common-imports.ts
import { RouterLink } from '@angular/router';
import { DatePipe, CurrencyPipe, AsyncPipe } from '@angular/common';
import { ButtonComponent } from './ui/button/button.component';
import { CardComponent } from './ui/card/card.component';
import { LoadingSpinnerComponent } from './ui/loading-spinner/loading-spinner.component';
export const COMMON_IMPORTS = [
RouterLink,
DatePipe,
CurrencyPipe,
AsyncPipe,
ButtonComponent,
CardComponent,
LoadingSpinnerComponent
] as const;
// Usage:
@Component({
standalone: true,
imports: [...COMMON_IMPORTS, ReactiveFormsModule],
// ...
})
export class CheckoutComponent { }
Granular Lazy Loading with @defer
By combining Standalone Components with the @defer block introduced in
Angular 17, you can achieve even more granular lazy loading at the template level:
<!-- The HeavyChart component loads only when it enters the viewport -->
@defer (on viewport) {
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<div class="chart-placeholder">Loading chart...</div>
} @loading (minimum 500ms) {
<app-loading-spinner />
}
<!-- The Comments component loads only on interaction -->
@defer (on interaction) {
<app-comments [postId]="postId()" />
} @placeholder {
<button>Show comments</button>
}
Benefits of Granular Lazy Loading
With NgModules, the minimum granularity for lazy loading was an entire module with all its
components. With Standalone Components, every single component can be loaded independently,
either through the router (loadComponent) or through the template
(@defer). This significantly reduces the initial bundle and improves
loading times.
10. Best Practices
After exploring all the features of Standalone Components, here are the best practices to follow for organizing code in modern Angular applications.
The 10 Golden Rules of Standalone Components
- Standalone by default: Always create standalone components. Use NgModules only for compatibility with legacy libraries
- Import only what you need: Avoid importing entire modules like
CommonModule. Import individual pipes and directives - Use the new template syntax: With
@if,@for, and@switchyou no longer need to importNgIf,NgFor,NgSwitch - One component, one file: Each standalone component should have its own file. Avoid declaring multiple components in the same file
- Barrel exports for features: Use
index.tsfiles to re-export shared components for a feature - Providers in routes, not components: For feature services, prefer
route.providersovercomponent.providers - providedIn: 'root' for global services: Keep
providedIn: 'root'for singleton, tree-shakable services - Aggressive lazy loading: Use
loadComponentin the router and@deferin templates to load components on demand - Migrate gradually: You do not have to migrate everything at once. Standalone and NgModules can coexist indefinitely
- Test every component in isolation: A major advantage of standalone is ease of testing. Make the most of it!
Recommended Project Structure
src/app/
app.component.ts # Root component (standalone)
app.config.ts # ApplicationConfig with providers
app.routes.ts # Main routes
core/ # Singleton services and guards
services/
auth.service.ts # providedIn: 'root'
api.service.ts # providedIn: 'root'
interceptors/
auth.interceptor.ts
guards/
auth.guard.ts
shared/ # Reusable components, directives, pipes
ui/
button/
button.component.ts # standalone
card/
card.component.ts # standalone
modal/
modal.component.ts # standalone
index.ts # barrel export
directives/
tooltip.directive.ts # standalone
pipes/
time-ago.pipe.ts # standalone
features/ # Application features
products/
product-list.component.ts # standalone
product-detail.component.ts # standalone
product-card.component.ts # standalone (local)
products.routes.ts # sub-routes
admin/
admin-layout.component.ts
dashboard.component.ts
admin.routes.ts
Testing Standalone Components
One of the most significant advantages is simplified testing. You no longer need to configure
a complex test module with TestBed: the component carries all its dependencies
with it.
// product-card.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductCardComponent } from './product-card.component';
describe('ProductCardComponent', () => {
let fixture: ComponentFixture<ProductCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
// With standalone, just import the component!
imports: [ProductCardComponent]
// No need for: declarations, module imports, etc.
}).compileComponents();
fixture = TestBed.createComponent(ProductCardComponent);
});
it('should display the product name', () => {
// Use ComponentRef to set inputs
fixture.componentRef.setInput('product', {
id: 1,
name: 'Angular T-Shirt',
price: 29.99
});
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.textContent).toContain('Angular T-Shirt');
});
it('should emit addToCart when the button is clicked', () => {
fixture.componentRef.setInput('product', {
id: 1,
name: 'Angular T-Shirt',
price: 29.99
});
fixture.detectChanges();
const spy = jest.spyOn(fixture.componentInstance.addToCart, 'emit');
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(spy).toHaveBeenCalledWith(expect.objectContaining({
id: 1
}));
});
});
When to Keep NgModules
Even though Standalone Components are the future, there are cases where NgModules are still useful or necessary:
- Third-party libraries: Some libraries still export NgModules. You can import them directly in the standalone component's
importsarray - Large legacy projects: If you have a project with hundreds of components, migration can be gradual. You do not need to do everything at once
- SharedModule with many dependencies: If you have a SharedModule that imports dozens of components, it may be more practical to keep it temporarily and replace it with a barrel export when you are ready
Summary and Next Steps
In this article we explored Angular Standalone Components in depth: from the motivation to migration, from routing to dependency injection. Here are the key takeaways:
Key Concepts
- Standalone Components eliminate the need for NgModules, making each component self-sufficient
- From Angular 19, standalone is the default behavior for components, directives, and pipes
- The
importsarray in the decorator allows declaring dependencies directly in the component loadComponentandloadChildrensimplify lazy loading without module wrappersbootstrapApplication()andApplicationConfigreplace module-based bootstrapping- The
provide*()functions replace configuration modules (RouterModule.forRoot(), etc.) - DI works at four levels: root, app config, route, and component
- The migration schematic automates the conversion in three progressive phases
- Barrel exports and common import arrays replace SharedModules
In the next article in the series, Signal Forms, we will see how the Signals revolution extends to form management, with a modern alternative to Reactive Forms that leverages fine-grained reactivity to create performant and declarative forms.
Related deep dive: 5 - Signal Forms: i Form Reattivi del Futuro.
Related deep dive: 6 - Angular SSR e Incremental Hydration.
Frequently asked questions
What is the main difference between Standalone Components and NgModules in Angular?
The main difference is that Standalone Components do not require declaration in an NgModule, whereas NgModules were previously used as the fundamental unit for code organization.
From which version of Angular are all components, directives, and pipes standalone by default?
Starting with Angular 19, all components, directives, and pipes are standalone by default.
How do you declare dependencies in a Standalone Component using the imports array in the decorator?
You declare template dependencies, such as other components, directives, pipes, and even entire NgModules (for compatibility with legacy libraries), directly in the imports array of the component's decorator.
What is the recommended way to provide a service in a Standalone Component using Dependency Injection?
The simplest and recommended way is to use the providedIn: 'root' property in the decorator, which registers the service as a singleton in the root injector.
Related deep dive: Zero Articoli Prerenderizzati: Perché il Prerender Totale è un Rischio di Build, non un'Ottimizzazione.
Commenti
Caricamento commenti...
Sign in to comment