Standalone Components Compilation vs NgModule Boundary
- Last updated
- Prerequisites:
- Angular components and NgModules
- Understanding of the Angular compiler (ngtsc)
- angular
- standalone
- ngmodule
- compilation
- component
- architecture
Read at your depth
The practical view
A standalone component declares its own dependencies directly: @Component({ standalone: true, imports: [CommonModule, MyComponent], ... }). It does not need to be declared in an NgModule. You can lazy-load it directly in routes: { path: 'x', loadComponent: () => import('./x.component').then(m => m.XComponent) }. Since Angular 17, standalone: true is the default and the recommended style. NgModules still exist for some cases (browser module config, NgModule-based libraries), but new code can be fully standalone. Remove declarations/imports from a module and use imports: [] on the component itself.
The same idea in other frameworks
React components are inherently standalone (import what you use); Angular's standalone mode converges to that model.standalone components — the current topic.Vue SFCs are self-contained by design, like Angular standalone.Svelte components import their dependencies directly — the same model.Legacy vs modern
NgModule-declared component vs standalone component
NgModule indirection requires declaring components in a module and importing the module everywhere; standalone components import dependencies directly.
@Component({ selector: 'app-card', template: '...' })
@NgModule({
declarations: [CardComponent], // ❌ OLD: must declare in a module
imports: [CommonModule],
})
export class CardModule {}@Component({
selector: 'app-card',
imports: [CommonModule], // ✅ NEW: dependencies declared here
template: '...',
})
export class CardComponent {}Interview gotchas
Context
This tests whether you understand the historical role of NgModules (compilation units, DI scope, tree-shaking boundaries) and how standalone components move that responsibility.
The mechanical answer
NgModules were the compilation and dependency-scoping unit: a component could only use directives/pipes listed in its module's imports, which defined both the compilation unit (what the compiler knows about) and the injector scope. This enabled ahead-of-time compilation with a bounded dependency set, but it also meant indirection (declare + import + export), harder tree-shaking, and heavier lazy chunks (a module loads all its declarations). Standalone components declare dependencies directly in imports: [], so the compiler resolves the exact dependency graph per component, enabling precise tree-shaking and component-level lazy loading. The DI scope still comes from the injector (environment + element), but there is no artificial module boundary to configure.
Trap
The naive answer is 'standalone just removes a file'. It changes compilation granularity and tree-shaking. Another trap: assuming NgModules are dead — existing libraries and some setup (BrowserModule, providers arrays) still use them; migration is mechanical but not zero. Candidates who mention compilation units, DI scope, and lazy-chunk size show real architectural understanding.
Context
This probes the runtime/compiler mechanics — how imports: [] becomes a render-time dependency list and how the injector tree is built for standalone components.
The mechanical answer
At compile time, ngtsc resolves each symbol in the imports array by reading its Angular metadata (ɵcmp/ɵdir/ɵpipe) and records the resolved directives/pipes in the component's ɵcmp def. At runtime, when the component is instantiated, Angular uses that pre-resolved dependency list to build the view (matching element tags to directives) without consulting any module. Providers declared on a standalone component (providers: []) are scoped to the component's element injector, so descendants can inject them but siblings cannot. If a standalone component is bootstrapped or lazy-loaded, it inherits the application's EnvironmentInjector; provider trees (providedIn: 'root') still work globally. The migration tool rewires these relationships automatically.
Trap
The naive answer is 'standalone components have no providers or injectors'. They create element injectors exactly like before. Another trap: assuming a standalone component can use any directive globally — it can only use what is in its imports array, and forgetting a directive import produces a template compile error (NG0301-style). Candidates who describe the metadata resolution and injector scoping show real under-the-hood knowledge.