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?
answer
- standalone pages import old modules
- lazy Routes array under forRoot
- route-level providers
- shared-module providers duplicate
basics
~20 sBuild 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.
solid answer
~40 sNothing else has to change: `bootstrapModule` and `RouterModule.forRoot` both work with standalone code. I build the reports pages as standalone components; they import existing NgModules such as `SharedModule` for legacy components, and new pieces directly. The app routes get an entry with `loadChildren` returning a plain `Routes` array, so there is no `ReportsModule` or `forChild`. Feature-only services go in that route's `providers`, and app-wide ones use `providedIn: 'root'`. The trap is a shared module with its own `providers`: when the router creates a standalone component, its imported modules' providers land in a standalone injector, giving the feature duplicate service instances, so I move those services to root first.
code
ts · 13 linesimport { Routes } from '@angular/router';
import { ReportsStore } from './reports-store';
export const REPORTS_ROUTES: Routes = [
{
path: '',
providers: [ReportsStore],
children: [
{ path: '', loadComponent: () => import('./reports-page').then((m) => m.ReportsPage) },
{ path: ':id', loadComponent: () => import('./report-detail').then((m) => m.ReportDetail) },
],
},
];go deeper
Know that standalone components can live inside a module-based app and can import existing modules to reuse their components.
Describe the lazy route with a Routes array, route-level providers, and why no ReportsModule or forChild is needed.
Anticipate the standalone-injector trap with shared modules that carry providers, and verify the lazy chunk and service identity after the change.
Use the first standalone feature as a pilot that proves tooling and conventions, and decide from it how aggressively to migrate the rest.
## The scenario A mature Angular app is entirely module-based: `AppModule` with `bootstrapModule`, an `AppRoutingModule` using `RouterModule.forRoot`, feature modules, and a `SharedModule` of common components. The team wants to build the next feature — a lazily loaded **reports** area — as standalone code, without converting anything else yet. This works because the router and the compiler do not care whether the surrounding app is module-based. The steps below keep the change inside the new feature. ## Step 1: build the feature as standalone components Generate the feature's components as standalone (the default since v19). Each lists its own template dependencies: ```ts @Component({ selector: 'app-reports-page', imports: [SharedModule, ReportChart], template: `<app-page-header title="Reports" /><app-report-chart [data]="data()" />`, }) export class ReportsPage { readonly data = inject(ReportsStore).summary; } ``` - **Old building blocks** come in by importing their module — here `SharedModule`, for `<app-page-header>`. The page sees only what `SharedModule` exports. - **New building blocks** such as `ReportChart` are standalone and imported directly. ## Step 2: add a lazy route in the existing routing module The existing `RouterModule.forRoot(routes)` accepts routes that load standalone code: ```ts const routes: Routes = [ // ...existing module-based routes using loadChildren... { path: 'reports', loadChildren: () => import('./reports/reports.routes').then((m) => m.REPORTS_ROUTES), }, ]; ``` `REPORTS_ROUTES` is a plain `Routes` array whose entries use `component` or `loadComponent`. No `ReportsModule` and no `RouterModule.forChild` are needed. ## Step 3: put feature services where they belong - A service used only in this feature can be listed in the parent route's `providers` array, which creates an environment injector for the reports routes. `providers: [ReportsStore]` on the `reports` route gives the feature one shared store that disappears from other areas. - A service used app-wide should be `@Injectable({ providedIn: 'root' })`. - A legacy library that only offers `SomeModule.forRoot()` can be bridged with `importProvidersFrom(...)` on that same route. ## Step 4: watch for the provider trap If `SharedModule` has its own `providers`, importing it into `ReportsPage` is not free. When the router creates a standalone component, Angular walks the component's import graph and puts any NgModule providers it finds into a **standalone injector** created for that component. The reports page would then get **its own copies** of `SharedModule`'s services — the same duplicate-singleton bug lazy NgModules have always had. The fix is to move those services to `providedIn: 'root'` (or a core module imported once) so that `SharedModule` carries declarables only. It is often the first piece of the migration worth doing anyway. ## What to leave alone for now | Keep as is | Why | |---|---| | `bootstrapModule(AppModule)` | The bootstrap style does not limit standalone features | | `RouterModule.forRoot` in `AppRoutingModule` | It happily loads standalone routes | | Other feature modules | They can keep working; convert them later, leaf components first | | `SharedModule` (minus its providers) | The new feature imports it until its pieces are standalone | ## How to verify the adoption - The reports chunk loads only when the route is visited — check the build output lists a separate lazy chunk for it. - The AOT build catches missing template imports as `NG8001` errors, so scope mistakes show up at build time. - Inject a shared service in both an old page and the reports page and confirm it is the same instance. ## Testing the new feature Standalone components are also simpler to test inside the old app. A `TestBed` configuration for `ReportsPage` imports the component itself — its template dependencies come along through its `imports` — instead of re-creating a testing module that declares the component and imports everything its template might need. Services such as `ReportsStore` are supplied through the test's `providers`, and a fake can replace the real one there. ## Why this is the recommended first step It gives the team hands-on standalone experience on new code, proves that the build, tests and lint rules handle standalone, and exposes provider problems in shared modules — all without touching the stable parts of the app. Subsequent migrations follow the same pattern in the other direction: convert leaf components and import them into their old modules.
- Why does the reports page get its own NotificationService when SharedModule provides it, even though AppModule also imports SharedModule?The router creates `ReportsPage` dynamically, so Angular gathers providers from the page's import graph, including `SharedModule`'s, into a standalone injector for that page. Those registrations are closer than the root ones, so the page tree resolves new instances. Moving `NotificationService` to `providedIn: 'root'` removes the duplicate.
- Does the reports feature need RouterModule.forChild or a ReportsModule?No. `loadChildren` can return a plain `Routes` array, and routes can point at standalone components with `component` or `loadComponent`. The existing `RouterModule.forRoot` configuration loads it like any other lazy branch.
It is like opening a new wing on an old building: the wing has modern wiring of its own, it connects to the existing corridors, and you move shared equipment to the central plant first so the new wing does not quietly install a second copy.
saying these in an interview costs you the question
- You must switch to bootstrapApplication before writing any standalone feature
- A lazy standalone feature needs its own NgModule with RouterModule.forChild
- Importing SharedModule into a routed standalone page never duplicates services
- Standalone pages cannot use components from existing NgModules
- Feature services must be declared in the root module