skip to content

Standalone & NgModule Interop

Using NgModules in a standalone component's imports, never declaring a standalone class, and bootstrapModule versus bootstrapApplication. Interviewers ask how a legacy app migrates piecemeal.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Angular, how do you use an NgModule's components inside a standalone component, and a standalone component inside an NgModule?

level: middleimportance: must knowfreq 64%

answer

  1. both directions use imports
  2. modules give only their exports
  3. never declare a standalone class
  4. modules can re-export standalone

basics

~20 s

Both directions use imports: a standalone component lists the NgModule in its own imports and gets that module's exports; an NgModule lists the standalone class in its imports, and may re-export it, but never declares it (NG6008).

solid answer

~40 s

A standalone component's `imports` array accepts NgModules, and it then sees exactly that module's `exports`, as any importer would. The reverse is also an import: an NgModule lists a standalone component, directive or pipe in its `imports`, and can add it to `exports` to share it. Putting a standalone class in `declarations` fails with `NG6008`, 'is standalone, and cannot be declared in an NgModule', and a standalone component cannot go in `bootstrap`. One trap: if a module imported by a routed or bootstrapped standalone component has providers, Angular collects them into a standalone injector for that component, which can create duplicate service instances.

code

ts · 21 lines
ts
import { Component, NgModule, input } from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common';
import { LegacyTableModule } from './legacy/legacy-table.module';
import { OrderList } from './orders/order-list';
import { InvoiceRow } from './orders/invoice-row';

@Component({
  selector: 'app-invoice',
  imports: [LegacyTableModule, CurrencyPipe],
  template: `<legacy-table [rows]="rows()" />`,
})
export class Invoice {
  readonly rows = input.required<InvoiceRow[]>();
}

@NgModule({
  declarations: [OrderList],        // standalone: false
  imports: [CommonModule, Invoice], // standalone classes are imported
  exports: [Invoice],
})
export class OrdersModule {}

go deeper

for a junior

Remember that both directions go through imports: modules into a standalone component's imports, and standalone classes into a module's imports, never declarations.

for a middle

Explain that importing a module gives only its exports, that modules can re-export imported standalone classes, and what NG6008 means.

for a senior

Spot the provider trap: modules with providers imported by routed standalone components create standalone injectors and duplicate service instances.

for a principal

Use the two interop directions to design a migration where converted leaf components are consumed by old modules and new features without a big-bang change.

## Two directions of interop Angular lets standalone components and NgModules coexist in one application, in both directions. That is what makes incremental migration possible: nothing forces a codebase to convert everything at once. Interviewers check that you know **which array** each direction uses, because the wrong choice is a compile error. | Direction | Where it goes | What you get | |---|---|---| | NgModule into a standalone component | the component's `imports` | the module's **exports** in that component's template | | Standalone class into an NgModule | the module's `imports` (never `declarations`) | the class usable in the module's declared templates | | Standalone class re-shared by a module | the module's `imports` **and** `exports` | the class visible to modules that import this module | ## An NgModule inside a standalone component A standalone component declares its template dependencies in its own `imports` array, and that array accepts NgModules as well as standalone classes: ```ts @Component({ selector: 'app-invoice', imports: [LegacyTableModule, CurrencyPipe], template: `<legacy-table [rows]="rows()" />`, }) export class Invoice { readonly rows = input.required<InvoiceRow[]>(); } ``` Rules to remember: - The component sees only the module's **exports**, exactly as another NgModule importing it would. Private declarations stay private. - Re-exports count: if `LegacyTableModule` re-exports another module, those exports come along. - If the imported module carries **providers**, they do not simply vanish. When the standalone component is created dynamically — bootstrapped, routed to, or created with `createComponent` — Angular collects providers from its import graph into a **standalone injector**, an environment injector created for that component. That can produce separate instances of services the module provides, which is why modules imported for their components should not carry singleton services. ## A standalone class inside an NgModule A module-based feature can use a standalone component, directive or pipe by **importing** it: ```ts @NgModule({ declarations: [OrderList], // standalone: false imports: [CommonModule, Invoice], // Invoice is standalone exports: [Invoice], // optional: share it with importers }) export class OrdersModule {} ``` - Putting a standalone class in `declarations` fails compilation with **`NG6008`**: "Component Invoice is standalone, and cannot be declared in an NgModule. Did you mean to import it instead?" - A module may **export** a standalone class it imports, so a shared module can keep handing the same building block to its importers while the component itself has become standalone. The official standalone migration produces exactly this shape. - A standalone component cannot be listed in an NgModule's `bootstrap` array (`NG6009`); a standalone root is started with `bootstrapApplication` instead. ## Why both directions matter for migration The two directions let a team convert **leaf components first**. A converted component keeps working inside the old modules (imported, and optionally re-exported), while new standalone features can pull in old modules for anything not yet converted. At no point does the app need a big-bang rewrite. A useful mental model: 1. Standalone classes are **imported everywhere**. 2. Declared classes are **declared once** and **exported** to be shared. 3. NgModules are an **import** unit for both worlds. ## Common mistakes - Declaring a standalone component "because it is used in this module". - Expecting a module's non-exported components to be usable after importing it into a standalone component. - Importing a module that provides singleton services into routed standalone components, and then seeing two instances. - Using `importProvidersFrom(SomeModule)` to get a module's **components** — that function collects providers only, and belongs in environment providers such as the application config. - Importing `CommonModule` into every new standalone component out of habit. Built-in control flow (`@if`, `@for`) needs no import, and a template that uses a pipe such as `date` can import just `DatePipe`. ## Checking your answer in code review When reviewing mixed code, read each array for its job: `declarations` should contain only `standalone: false` classes; every standalone class reaches a module through `imports`; a standalone component's `imports` may list modules, but only for their exports. If a shared module both provides services and is imported by routed standalone pages, move the services to `providedIn: 'root'` before it causes duplicate state.

  • Why can a module's providers produce duplicate services when a routed standalone component imports that module?
    When a standalone component is created dynamically, for example by the router, Angular walks its import graph and, if any imported NgModule has providers, registers them in a standalone injector created for that component. Those registrations shadow root instances, so the component tree below gets its own copies. Services meant as singletons should use `providedIn: 'root'`.
  • Can an NgModule export a standalone component it does not declare?
    Yes. A module may export anything it imports, including standalone components, directives and pipes. This lets an existing shared module keep offering a component to its importers after that component has been converted to standalone, which is how a migration avoids touching every consumer at once.

saying these in an interview costs you the question

  • A standalone component used in a module must be added to declarations
  • Importing an NgModule into a standalone component exposes all its declarations
  • Standalone components and NgModules cannot be mixed in one application
  • importProvidersFrom is how a standalone component uses a module's components
  • An NgModule can only export components it declares itself
open as a page

In Angular, how does bootstrapModule(AppModule) differ from bootstrapApplication(App, appConfig), and can a module-based app use provideRouter or provideHttpClient?

level: middleimportance: should knowfreq 50%

basics

~20 s

bootstrapModule starts an app from a root NgModule whose bootstrap array names the root component; bootstrapApplication starts from a standalone root plus an ApplicationConfig. Module-based apps can use provideRouter and provideHttpClient in the root module's providers.

open as a page

A module-based Angular app wants its first standalone feature, a lazily loaded reports area; how do you add it without converting the rest of the app?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Build the reports pages as standalone components that import existing NgModules for old pieces, load them through a lazy Routes array from the existing RouterModule.forRoot config, and provide feature services on the route. Keep shared modules free of providers to avoid duplicates.

open as a page

How would you sequence an incremental standalone migration of a large module-based Angular app so that every intermediate step ships safely?

level: principalimportance: should knowfreq 32%

basics

~20 s

Fix provider hygiene first, write new features standalone, convert leaf declarables and import them into their old modules, delete emptied modules, and switch bootstrapModule to bootstrapApplication last. Interop keeps every step shippable, and stopping early is a legitimate choice.

open as a page