Email copiado — support@tuurt.com
Cargando experiencia
angular · September 18, 2026 · 6 min

Standalone Components in Angular: What You Actually Gain by Dropping NgModules

Standalone components declare their own dependencies right in the decorator, no container NgModule required. We look at what changes in the dependency tree, in per-route lazy loading, and what went wrong migrating a whole SharedModule in one shot.

By Tuurt Team

Standalone components in Angular: what you actually gain by dropping NgModules

For years, every Angular component needed an NgModule to declare it. Building a new screen almost always meant an extra .module.ts file: an NgModule with declarations, imports, and, if the route was lazy-loaded, a RouterModule.forChild with its own child routes. That file carried no business logic. It was plumbing so the compiler could resolve which directives and components a template was allowed to use. Since standalone components shipped as a developer preview in Angular 14 and became stable in Angular 15, that plumbing became optional, and since Angular 17 the CLI scaffolds new projects without NgModules by default. We migrated several internal projects to this model, and here is what actually changed in practice, not just on paper.

What a component really declares

A standalone component is marked with standalone: true and lists its own dependencies directly in the decorator's imports array:

@Component({
  selector: 'app-order-detail',
  standalone: true,
  imports: [NgIf, NgFor, RouterLink, OrderLineComponent],
  templateUrl: './order-detail.component.html',
})
export class OrderDetailComponent {
  order = input.required<Order>();
}

The difference from the NgModule model is not just syntax. Before, for this template to use *ngIf or <app-order-line>, someone had to declare OrderDetailComponent inside an NgModule that also imported CommonModule and declared or imported OrderLineComponent. What was available in a template depended on which container module chose to import what, and that container module often ended up importing more than it should: a SharedModule that started with two or three common pieces and ended up dragging in half a dozen third-party modules that most of its consumers never needed, simply because at some point someone dropped them in there instead of touching each feature module individually.

With standalone components, that question is answered by looking at a single file: the component's own imports array. If something is not listed there, it is not available in the template, no matter what other module elsewhere in the app happens to import it. The dependency tree stops being inherited from a container and becomes something each component declares for itself. This makes the imports array longer for components that use several common directives -NgIf, NgFor, AsyncPipe- because each one now has to ask for them explicitly instead of getting them for free through a parent module's CommonModule. In exchange, figuring out what a component actually uses no longer requires tracing the chain of modules wrapped around it.

Lazy loading a route without a module in between

The change that simplified our route configuration the most was loadComponent. Before, a lazy route pointed to a feature NgModule:

{
  path: 'orders',
  loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule),
}

And that OrdersModule existed almost exclusively to declare its own child routes and group the feature's components together. With standalone components, the route can point straight at the component:

{
  path: 'orders',
  loadComponent: () => import('./orders/orders-list.component').then(m => m.OrdersListComponent),
  children: [
    {
      path: ':id',
      loadComponent: () => import('./orders/order-detail.component').then(m => m.OrderDetailComponent),
    },
  ],
}

The chunk Angular generates for that lazy route contains exactly the component and its declared dependencies, without the intermediate layer of a module that only existed to group routes. For services that used to be registered with providedIn scoped to a feature module -meant to live only while that section of the app was loaded- the standalone equivalent is declaring the provider directly on the route definition through the providers property, which Angular instantiates and tears down together with that branch of the route tree.

What didn't work: migrating the SharedModule in one go

The first time we migrated a real project, we tried converting the entire SharedModule in a single pass: remove the NgModule, turn every piece it exported into a standalone component or directive, and update every component that imported it to instead import the individual pieces it actually needed. On paper it looked reasonable. In practice, that SharedModule was imported by close to forty components across different features, and several of them relied on it transitively re-exporting things it didn't import directly -an internal MaterialModule bundling half a dozen Angular Material modules, for instance. Removing the NgModule broke all of those components' compilation at once, and the migration diff became impossible to review carefully: forty files changed at the same time, with no clean way to tell which breakage belonged to which component.

Angular offers a bridge for exactly this, importProvidersFrom, and standalone components can keep importing an existing NgModule in their own imports array without any issue -an NgModule is, in that sense, a perfectly valid import for a standalone component. The fix was exactly that: leave SharedModule alive, migrate feature components to standalone one at a time while they kept importing SharedModule as-is, and only at the end, once every consumer had already been migrated and was visible as an individual component, take SharedModule apart into separate pieces -because by then we knew exactly which component needed which piece. The incremental migration took more calendar time than the one-shot attempt, but every single step was reviewable and reversible, and none of them broke the application in production.

When an NgModule still makes sense

We didn't migrate everything. Two concrete cases where we deliberately kept NgModules:

Internal libraries that bundle several related components, directives, and pipes so other teams can consume them through a single grouped import -a corporate UI component library, for example- still make sense as an NgModule, because the whole point of that library is to offer a grouped unit of import, and turning it into a list of individual imports just pushes onto the consumer a decision the library exists to make for them.

Legacy code that still depends on patterns tied to NgModules -for example, providers registered at the feature-module level with initialization logic living in the module's own constructor- doesn't benefit from a rushed migration. Converting that code to standalone without first understanding why that provider was registered there instead of at root can silently change when that instance is created and destroyed, and that is exactly the kind of subtle regression a UI test won't catch but that shows up weeks later as a shared-state bug between screens that shouldn't be sharing anything.

The rule we apply now

For new projects, there is no question: standalone from the very first component, no NgModules unless a third-party library still requires them. For existing projects, we migrate feature by feature, letting standalone components coexist with the NgModules we haven't touched yet -Angular supports that coexistence without friction- and we save tearing apart large shared modules for last, once we can clearly see what each consumer actually needs. The real gain isn't less code in any single component -the explicit imports array is sometimes longer than the implicit import you got before- it's that the question "where does what my component uses actually come from" now has exactly one possible answer: the component itself.

angular standalone-components ngmodules frontend
← Back to blog