Angular Standalone Components: Why They Matter
Why standalone components became Angular's default architecture, and how to structure an app around them.
For most of Angular’s history, every component, directive, and pipe had to belong to an NgModule. You couldn’t use a component without first registering it in some module’s declarations array, and you couldn’t use that module’s exports without importing the module somewhere else. It worked, but it was a layer of bookkeeping that didn’t map to anything a developer coming from React or Vue would recognize, and it was a common source of confusion for people new to the framework (“why doesn’t my component show up — oh, I forgot to add it to declarations”).
Standalone components remove that layer. A component declares its own dependencies directly, and Angular can bootstrap, route to, and lazy-load it without any module wrapping it. As of recent Angular versions, standalone is the default when you generate a new project — NgModules still work, but they’re no longer the thing you reach for first.
NgModules vs Standalone
The NgModule version of a simple feature looks like this:
// user-profile.module.ts
@NgModule({
declarations: [UserProfileComponent],
imports: [CommonModule, RouterModule.forChild([
{ path: '', component: UserProfileComponent },
])],
})
export class UserProfileModule {}
// user-profile.component.ts
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
})
export class UserProfileComponent {}
Two files coordinating one piece of UI, and the component itself can’t tell you what it depends on — you have to go check the module. The standalone version collapses this:
// user-profile.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-user-profile',
standalone: true, // default and implicit in recent Angular versions
imports: [CommonModule, RouterLink],
templateUrl: './user-profile.component.html',
})
export class UserProfileComponent {}
Everything the template needs — directives, pipes, other components — is listed right there in imports. Opening the file tells you the whole story. There’s no module to keep in sync, no risk of a component being declared in one module but accidentally used (and failing) in a template belonging to a different module that never imported it.
Bootstrapping Without a Root Module
The old entry point bootstrapped a root AppModule:
// main.ts (NgModule style)
platformBrowserDynamic().bootstrapModule(AppModule);
With standalone components, you bootstrap a component directly and configure the application through providers:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig);
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])),
],
};
This is the other half of the shift: things that used to be configured by importing a module into AppModule (RouterModule.forRoot(), HttpClientModule) are now configured with functional providers — plain functions that return provider configuration, passed into bootstrapApplication. provideRouter replaces RouterModule.forRoot/forChild, provideHttpClient replaces HttpClientModule, and most first-party and third-party libraries now ship an equivalent provideX() function.
Functional Providers in Practice
The functional provider pattern composes well because each provideX function can take its own configuration functions:
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([authInterceptor]), withFetch()),
provideAnimations(),
provideClientHydration(), // for SSR apps
],
};
Compare this to the NgModule equivalent, which needed RouterModule.forRoot(routes, { bindToComponentInputs: true }), HttpClientModule, BrowserAnimationsModule, and BrowserTransferStateModule all imported into AppModule’s imports array alongside every other cross-cutting module the app needed. The functional style is flatter, easier to tree-shake, and easier to read as a single list of “what capabilities does this app have.”
Lazy Loading Standalone Components
This is where standalone components noticeably simplify things compared to the module era. Previously, lazy-loading a feature meant creating a feature module just so the router had something to loadChildren against, even if that feature was really just one component. Now you can lazy-load the component itself:
export const routes: Routes = [
{
path: 'profile',
loadComponent: () =>
import('./user-profile/user-profile.component').then(
(m) => m.UserProfileComponent,
),
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
];
loadChildren still works for bigger features with their own route trees (just pointing at an exported Routes array instead of a module now), and loadComponent covers the common case of a single routed component. Both produce separate chunks, which is a meaningful part of the performance story covered in Angular Performance Optimization: Practical Techniques.
Migrating an Existing NgModule App
You don’t need to migrate everything at once, and in most real codebases you shouldn’t try to. Standalone components and NgModules can coexist:
- A standalone component can be declared in an
NgModule’simportsarray (notdeclarations— that’s the tell that it’s standalone). - An NgModule-declared component can still be used inside a standalone component’s template, as long as the module that exports it is listed in that component’s
imports. - The Angular CLI ships a schematic to automate a chunk of this:
ng generate @angular/core:standalonewalks your project and converts components, directives, and pipes, then can remove now-empty modules.
A pragmatic migration order that’s worked well in practice: convert leaf components first (ones with few dependents), work your way up to feature modules, and leave AppModule/root bootstrapping for last since it has the widest blast radius if something goes wrong. Run your test suite after each batch rather than converting the whole app in one pass — the schematic is good, but it can’t always infer every implicit module dependency correctly, especially around shared/barrel modules.
When NOT to Rush It
If you’re on an older Angular major with a large, stable NgModule codebase and no near-term plans for a bigger rewrite, there’s no urgency to convert everything today. NgModules aren’t deprecated, and a working app with good module boundaries isn’t broken. The stronger argument for standalone is for new feature work — write new components as standalone even in an otherwise NgModule-based app, since they interoperate fine, and it reduces the amount of NgModule boilerplate you’re adding going forward.
Best Practices
- Default to
standalone: true(or the implicit default in newer Angular versions) for all new components, directives, and pipes. - Keep each component’s
importsarray scoped to what it actually uses in its own template — don’t recreate “shared module” sprawl by importing a huge barrel of unrelated directives. - Use
provideRouter/provideHttpClient/etc. inapp.config.tsrather than reintroducingRouterModule.forRoot-style module imports. - Prefer
loadComponentfor single routed components andloadChildrenpointed at aRoutesarray for larger lazy-loaded features. - When migrating, convert incrementally and run tests between batches rather than converting an entire app at once.
FAQ
Do I still need CommonModule imports for things like *ngIf and *ngFor?
If you’re using the newer built-in control flow (@if, @for, @switch), no import is needed at all — it’s built into the template compiler. If you’re using the structural directive syntax, you still import CommonModule (or the more granular NgIf/NgFor directives) into the component’s imports array.
Can a standalone component inject services the same way?
Yes — inject() and constructor injection both work identically. Standalone components change how components declare template dependencies and how the app is bootstrapped; the dependency injection system itself is unchanged.
Is TypeScript strictness affected by standalone components? Not directly, but since standalone components tend to produce smaller, more explicit files, you’ll generally get better type inference and clearer compiler errors — related general guidance is in TypeScript Best Practices.
Related Reading
Written by
Tayyab Roy
Senior Frontend / Full-Stack Engineer
Senior Frontend & Full-Stack Engineer specializing in Angular, React, Next.js and enterprise eCommerce (Shopify, SAP Commerce / Hybris, Spartacus). I write about practical, production-tested frontend engineering.
Related Articles
NgRx vs Signals: Choosing State Management
When a full NgRx store still earns its complexity in an Angular app, and when Signals-based state is simpler and sufficient.
RxJS vs Signals in Modern Angular Applications
How RxJS and Signals coexist in modern Angular apps, and practical patterns for using both together.
Angular Performance Optimization: Practical Techniques
Practical, production-tested techniques for speeding up Angular applications — from change detection to bundle size.