Angular Standalone Components: Migration & Architecture Patterns
Standalone components landed in Angular 14 and became the default in v17. They remove NgModule boilerplate, speed up builds, and make lazy loading much simpler. This tutorial walks through a phased migration strategy plus patterns for routing, dependency injection, and testing in a fully standalone codebase. If you are also overhauling state, bookmark the Angular Signals guide so both migrations stay in lockstep.
Why Move Away From NgModules?
- Smaller bundles: You import only what a component needs instead of entire feature modules.
- Simpler mental model: Each component declares its own dependencies, so onboarding new teammates is easier.
- Better tooling: The CLI and schematic generators now default to standalone, making future upgrades smoother. Angular's official standalone component docs track API updates as they land.
Migration Strategy
- Freeze API surface – Capture current module boundaries so you know which components ship together.
- Convert leaf components first – Start with components that are only referenced by one module to reduce blast radius.
- Remove feature modules progressively – Once every component inside a module is standalone, delete the module and wire routes directly to the components.
- Refactor shared utilities last – Pipes, directives, and shared UI kits can also become standalone exports.
Convert a Component
ng g @schematics/angular:component dashboard/widgets/metric-card --standalone --change-detection OnPush
Angular CLI adds the standalone: true flag and an imports array. Update existing components manually like this:
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TrendBadgeComponent } from '../trend-badge/trend-badge.component';
@Component({
selector: 'app-metric-card',
standalone: true,
imports: [CommonModule, TrendBadgeComponent],
templateUrl: './metric-card.component.html',
styleUrls: ['./metric-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MetricCardComponent {
// component logic
}
Routing Without Modules
import { Routes } from '@angular/router';
import { DashboardPageComponent } from './pages/dashboard-page.component';
import { SettingsPageComponent } from './pages/settings-page.component';
export const appRoutes: Routes = [
{
path: '',
loadComponent: () => import('./pages/dashboard-page.component').then(m => m.DashboardPageComponent),
},
{
path: 'settings',
loadComponent: () => import('./pages/settings-page.component').then(m => m.SettingsPageComponent),
},
];
loadComponentreplacesloadChildrenfor lazy-loaded standalone routes. Each route file exports a single component, keeping bundles razor-thin. Need to hydrate those components with live data? Reuse the HttpClient playbook from Angular Services & HttpClient so each lazy chunk fetches exactly what it needs.
Provide Services With the @Injectable Decorator
Most services can rely on providedIn: 'root'. When you need scoped services (per feature or dialog), use the providers array directly on the standalone component.
@Component({
selector: 'app-invoice-sheet',
standalone: true,
imports: [CommonModule],
providers: [InvoiceSheetStore],
templateUrl: './invoice-sheet.component.html',
})
export class InvoiceSheetComponent {
constructor(readonly store: InvoiceSheetStore) {}
}
For form-heavy flows, remember you can import the utilities from Angular Reactive Forms directly into the imports array of any standalone component.
Organize Feature Directories
app/
dashboard/
pages/
dashboard-page.component.ts
components/
metric-card/
chart-widget/
services/
dashboard.store.ts
settings/
pages/
components/
pages/contains top-level route components.components/holds reusable widgets that can be imported anywhere.services/contains feature stores or facades.
Testing Standalone Components
Angular's testing utilities now accept standalone components directly:
import { render } from '@testing-library/angular';
import { MetricCardComponent } from './metric-card.component';
it('renders the trend badge', async () => {
const { getByText } = await render(MetricCardComponent, {
componentProperties: {
heading: 'Revenue',
value: '$42k',
delta: 18,
},
});
expect(getByText('Revenue')).toBeTruthy();
});
No TestBed.configureTestingModule is required; the test harness uses the component's imports array under the hood.
Common Pitfalls
- Half-migrated modules: Converting a component but leaving the NgModule untouched leads to double registration. Remove NgModule declarations as soon as the component is standalone.
- Circular imports: Standalone components importing each other can recreate dependency cycles. Use shared utility modules or tokens to break loops.
- Global providers: Forgetting to mark services with
providedIncan lead to missing injection errors once modules disappear. - Third-party libraries: Many component libraries now export standalone variants. Always import those instead of the module wrappers.
Checklist Before Deleting a Module
- Every declaration inside the module is now standalone.
- Routes are defined via
loadComponentor run throughprovideRouter. - Shared pipes/directives export their own
standalone: truecomponents. - Lazy-loaded chunks still split correctly (check
ng build --configuration production).
Frequently Asked Questions
Do I need bootstrapApplication to use standalone components?
You can bootstrap with bootstrapApplication(AppComponent, { providers: [...] }) for full standalone mode. Existing NgModule bootstraps still work, but consider switching for simpler providers.
How do I share Angular Material modules?
Angular Material now publishes standalone imports (e.g., MatButtonModule can be replaced with MatButton). Import those directly inside your component imports array.
What about guards and resolvers?
Use canActivate: [inject(AuthGuard)] inline or leverage the functional guard APIs introduced in Angular 15. They work seamlessly with standalone routes.
Conclusion
Standalone components shrink Angular apps and make feature code easier to reason about. Migrate leaf components first, embrace loadComponent routing, and keep services scoped through providers on the component itself. With those patterns, future Angular upgrades become boring—which is exactly what you want.
Next Steps
- Audit every feature module and list which components still rely on it.
- Convert shared pipes/directives to standalone exports.
- Combine this tutorial with the Signals guide to modernize change detection at the same time.
Additional Resources
- Angular Routing & Navigation – revisit guards, resolvers, and preloading strategies as you refactor routes.
- Angular Services & HttpClient – reinforce DI and networking patterns post-module removal.
- Angular Standalone API docs – canonical reference straight from the Angular team.
- Angular Update Guide – auto-generated checklist for every upgrade step.