Standalone Component Imports: Angular Fix
Add the missing piece to the imports array.
20+ years shipping production backend systems. Everything here is grounded in real deployments.
- ✓Angular components and modules
- ✓Basic routing with routerLink
- ✓An Angular CLI project to edit
- Standalone components have no NgModule, so every child component, directive, pipe, and helper module must sit in their own imports array
- NgModule apps share dependencies per module; standalone apps declare them per component, and children never inherit imports
- Router directives need RouterModule imports and ngIf/ngFor need CommonModule in each using component
- Migrate leaf-first with production AOT builds, since mocked specs never compile the real imports arrays
Think of NgModule apps as office kitchens stocked per floor: one shared pantry serves every team on that floor. Standalone components are lunch boxes: each person packs exactly what they'll eat. Converting to standalone without packing the imports is showing up with an empty box and wondering where lunch went. The fix is packing deliberately: every selector your template uses goes in your imports box, and shared snacks like router links need packing too.
You convert a component to standalone, save, and the template fills with red: 'app-avatar' is not a known element. The component exists. It worked yesterday. The only change is the standalone flag, and suddenly Angular can't see a file sitting right next to it.
Standalone components carry no NgModule, so nothing supplies their template dependencies implicitly. Every child component, directive, pipe, and helper module must appear in the component's own imports array. Yesterday AppModule provided AvatarComponent to the whole feature; today nobody provides it to your template. The compiler is right: from its scope, that selector doesn't exist.
Migration multiplies the confusion. Half the codebase still lives in modules while the other half imports directly, and each dependency demands the wiring style its author chose. Router directives, CommonModule structural directives, and third-party UI pieces each fail in their own dialect of the same missing-import error.
This guide untangles it. You'll learn the standalone imports mental model, how it differs from NgModule wiring, which entries router and structural directives need, how to mix both eras safely, and how to migrate feature by feature without breaking half your routes.
The Standalone Imports Mental Model
A standalone component's imports array is the complete list of what its template may use. Child components, attribute directives, structural directives via their modules, and pipes all enter scope exclusively through this array. Nothing is inherited from parents, nothing leaks sideways from siblings, and nothing arrives globally. When the compiler reports an unknown element, it means exactly what it says: within this template's scope, that selector was never registered.
This explicitness replaces the old transitive visibility where importing a module granted its entire export surface to every template in the feature. The new model trades that convenience for precision: templates declare their true dependencies, the bundler drops everything else, and readers see the full dependency list without chasing module files. The cost is repetition: a component used in ten templates appears in ten imports arrays, which feels verbose until you realize each entry is documentation.
Build the verification habit mechanically. For every selector in the template, find its owner and confirm the import entry. Automate the check with lint rules that flag unknown selectors at PR time, and keep a snippet that scaffolds new components with the common imports pre-listed. Developers who map selectors to imports as they type never meet this error; developers who add imports after the fact meet it weekly.
NgModule vs Standalone Wiring Compared
NgModule wiring and standalone wiring solve the same scoping problem from opposite ends. A module declares its components once and imports shared dependencies once for all its templates. A standalone component imports its own dependencies once for its own template. Mixed codebases must respect both directions: modules consume standalone components through their imports array, while standalone components consume modules, or other standalone pieces, through theirs.
Two crossings are illegal and produce confusing errors. A non-standalone component can't appear in a standalone imports array; import its owning module instead. A standalone component can't appear in a module's declarations; list it under imports. The error messages name the violation precisely, but developers misread them as missing-dependency errors and add more wrong entries. Read the message literally: it states which crossing you attempted.
Migration strategy follows the dependency leaves inward. Convert leaf components with no children first: their imports arrays are small and verifiable. Then convert features whose children are already standalone. Convert bootstrap and routing last, when providers can move to functional equivalents. Verify each step with a production build, because dev-server tolerance hides wiring gaps that AOT exposes at the worst moment.
Router and Structural Directives Need Imports Too
Router directives surprise migrators because routing feels like infrastructure that should just work. It doesn't: routerLink, routerLinkActive, and router-outlet are directives like any other, entering standalone templates only through RouterModule imports. A component that navigated fine under a routing module fails standalone with unknown-property errors on routerLink, while the route configuration itself remains perfectly valid.
Structural directives follow the same rule through CommonModule. A standalone template using ngIf or ngFor without importing CommonModule errors on the directive while custom selectors resolve fine, which sends developers hunting through component imports for a problem in module imports. Memorize the pairing: control flow needs CommonModule, two-way forms need FormsModule, routing needs RouterModule. Each is one import entry beside your components.
Scaffold discipline prevents the whole category. Team snippets for new standalone components should pre-import CommonModule and RouterModule where the feature needs them, leaving developers to add only business components. Review checklists should ask which directives the template uses, not just which components. Directives are invisible in the rendered page but mandatory in the imports array.
Providers Move Too: the importProvidersFrom Bridge
Providers follow a parallel migration path that intersects imports at bootstrap. NgModules contributed providers through their imports; standalone apps contribute them through the providers array in app.config.ts or bootstrapApplication. Deleting a module import during migration removes its provider recipes as well as its directives, which is how a component conversion produces a NullInjectorError two routes away from the edited file.
importProvidersFrom is the sanctioned bridge for half-migrated apps. It pulls an unmigrated module's providers into the standalone bootstrap context without converting the module itself. Use it for third-party modules lacking functional providers, and replace each bridge with a functional equivalent like provideHttpClient() as libraries modernize. Every bridge deserves a comment naming the module and the planned replacement, or bridges become permanent.
Audit deletions against additions during every migration PR. For each removed module import, answer two questions: which directives did it supply, and which providers did it register. Directives move to component imports arrays; providers move to app.config.ts. PRs that answer both explicitly merge safely; PRs that move syntax without tracing semantics break routes. The checklist is short but non-negotiable.
Third-Party Libraries Across Both Eras
Third-party libraries split across both eras, and each demands its own wiring. A standalone UI component imports directly into your imports array. A module-based UI kit imports as a module, granting its exported components to your template. Guessing wrong produces errors about unexpected imports or unknown elements that look like version bugs but are really style mismatches.
The decorator is the ground truth when docs lag. Open the library source or its type definitions and check for standalone: true on the component. Present means direct import; absent means import the owning module. Docs for popular kits usually state the style per version, but transitive dependencies and beta releases often trail behind. Thirty seconds reading the decorator beats thirty minutes fighting the compiler.
Pin and verify library wiring in CI. A production build over a lockfile catches renames, style changes, and dropped exports before they reach main. When upgrading UI kits, grep templates for affected selectors first and update them in the same commit as the version bump. Library imports are contracts with external authors; treat version bumps as renegotiations, not chores. Renegotiate carefully and the integration stays quiet for months.
The Selector Roll Call That Prevents All of This
A systematic pass prevents wiring bugs from surviving to build time. Read the template top to bottom, list every custom selector, directive, and pipe, and map each to its owning file and standalone status. Open the imports array and check off each entry. Unchecked items are future compiler errors; check them now while the template's intent is fresh in mind.
Do this pass at creation time, not review time. The author holds the full context of which pieces the template needs, while reviewers reconstruct it from diffs. A two-minute roll call during writing saves a twenty-minute debugging session later plus the CI cycle in between. Make it part of the definition of done for every standalone component.
Enforce it mechanically where humans slip. Lint rules flag unknown selectors at PR time, real-template specs fail on missing imports, and production builds verify the whole graph. Human discipline starts the habit; automation keeps it alive across team growth and deadline pressure. Components maintained this way import exactly what they use, no more and no less. Roll calls take two minutes and save twenty; make them automatic and the savings compound across every sprint and every new hire.
The Codemod That Emptied Every Header for 41 Minutes
- Codemods move syntax, not semantics: every selector in every template needs a human-verified imports entry after conversion.
- Mocked specs can't validate imports arrays, so shared components need specs compiling real templates with real imports.
- Production AOT builds in CI catch wiring errors in the PR that introduced them, when the selector context is still fresh.
| File | Command / Code | Purpose |
|---|---|---|
| src | @Component({ | The Standalone Imports Mental Model |
| src | @NgModule({ | NgModule vs Standalone Wiring Compared |
| src | @Component({ | Router and Structural Directives Need Imports Too |
| src | export const appConfig: ApplicationConfig = { | Providers Move Too |
Key takeaways
Common mistakes to avoid
5 patternsPutting a standalone component in providers instead of imports
Forgetting RouterModule for routerLink and routerOutlet
Importing an NgModule-based component directly
Declaring a standalone component in an NgModule
Using ngIf or ngFor without importing CommonModule
Interview Questions on This Topic
Why must standalone components list their own imports?
Frequently Asked Questions
20+ years shipping production backend systems. Everything here is grounded in real deployments.
That's Angular. Mark it forged?
5 min read · try the examples if you haven't