skip to content

You own a v15-era Angular codebase now updated to v22; in what order would you run the modernization schematics, feature by feature, and why?

level: seniorimportance: must knowfreq 46%

answer

  1. update first, modernise second
  2. templates before modules
  3. DI and signals per folder
  4. cosmetics last, tests between

basics

~20 s

Run the optional schematics after the version update, one feature folder at a time: control flow, then the three standalone modes, then inject(), then the signal input, output and query migrations, and self-closing tags last, testing between each.

solid answer

~40 s

The `ng update` hops come first and only keep behaviour; the modernisation schematics are separate, opt-in `ng generate @angular/core:<name>` runs. I would do them per feature folder using each schematic's `path` option, committing after each: **control flow** first, since it removes `NgIf`-style imports and so shrinks what the standalone step must add; then **standalone** in its three modes (`convert-to-standalone`, `prune-ng-modules`, `standalone-bootstrap`, the last once for the whole app); then **inject()**, with `backwardsCompatibleConstructors` if decorated classes inherit from each other; then the **signal migrations** for inputs, outputs and queries with `--insert-todos`; and finally **self-closing tags** and import clean-up, which are cosmetic. Tests and a build run between every step, and the `track` expressions and TODOs each step leaves become tracked follow-up work, not part of the mechanical diff.

go deeper

for a junior

Know that modernisation schematics are separate from version updates and that you run them with ng generate @angular/core.

for a middle

Explain what each schematic converts and why control flow and the standalone modes come before cosmetic passes.

for a senior

Lay out the folder-by-folder plan with builds and tests between steps, separate commits, and a tracked backlog of what each step skips.

for a principal

Decide how much modernisation is worth doing now versus letting code converge as teams touch it, and how to keep feature delivery going meanwhile.

A codebase written around v15 typically has `NgModule`s everywhere, `*ngIf`/`*ngFor` templates, constructor injection and `@Input`/`@Output`/`@ViewChild` decorators. After the version updates to v22 it still compiles, because the update migrations preserve behaviour, but it looks nothing like current Angular. Modernising it is a sequence of **optional schematics** in `@angular/core`, and the order and granularity decide how painful it is. ## Separate the update from the modernisation - **Version updates** (`ng update`, one major at a time) apply the migrations Angular registers for each version. They keep behaviour: the v22 one, for example, adds `ChangeDetectionStrategy.Eager` to components that set no strategy. - **Modernisation schematics** are run on purpose with `ng generate @angular/core:<name>`. None of them runs automatically. Mixing the two makes every regression ambiguous. Finish the update, get the build green on v22, then start modernising. ## A sensible order | Step | Schematic | Why here | | :-- | :-- | :-- | | 1 | `control-flow` | Removes `NgIf`/`NgForOf`/`NgSwitch` usage, so later steps add fewer imports | | 2 | `standalone`, `mode=convert-to-standalone` | Declarations become standalone with their own `imports` | | 3 | `standalone`, `mode=prune-ng-modules` | Deletes modules that are now empty | | 4 | `standalone`, `mode=standalone-bootstrap` | Once, for the whole app, at the end of the standalone work | | 5 | `inject` | Constructor parameters become `inject()` fields | | 6 | `signal-input-migration`, `output-migration`, `signal-queries-migration` (or the combined `signals`) | Decorator APIs become functions, references updated | | 7 | `self-closing-tag`, `cleanup-unused-imports`, `common-to-standalone` | Cosmetic and tidy-up passes | The docs do not prescribe this exact sequence, and other orders work. The reasoning is what matters: structural changes before cosmetic ones, template syntax before module structure so the standalone step computes smaller `imports`, and the bootstrap switch only once everything else is standalone. ## Feature by feature Most of these schematics accept a **`path`** option: 1. Pick one feature folder with good test coverage as the pilot. 2. Run steps 1-3 and 5-6 for that folder, building and running tests after **each** schematic. 3. Commit each schematic's output separately, so reviewers read one kind of change per commit. 4. Repeat folder by folder; run step 4 once, when all features are standalone. The signal migrations still analyse the whole workspace by default when you pass `--path`, so references in other folders are updated correctly. Avoid narrowing `--analysis-dir`, which silently skips outside references. ## Options worth knowing per step - **inject**: `migrateAbstractClasses` (off by default, because Angular does not validate abstract class parameters); `backwardsCompatibleConstructors` (keeps a `constructor(...args: unknown[])` signature when decorated classes extend each other); `nonNullableOptional` (adds `!` after `inject(X, {optional: true})` to keep the old non-null type, at the cost of hiding real nullability). - **signal migrations**: `--insert-todos` to record why fields were skipped; `--best-effort-mode` only on a clean branch. - **control flow**: `format` to reformat templates; review generated `track` expressions. ## What each step leaves behind - Control flow: `track item` identity tracking, preserved `ng-template`s, possibly `CommonModule`. - Standalone: modules with `providers`, `TODO(standalone-migration)` comments, test `imports` that may be wrong. - inject: abstract classes, unless you opted in. - Signals: skipped inputs and queries (written-to fields, accessors, `@HostBinding`, `spyOn`), outputs used with `.pipe()`. Treat these as a backlog with owners, not as noise in the migration commits. ## What not to do - Run every schematic over the whole repository in one branch. - Combine modernisation with feature work in the same pull request. - Remove the `Eager` the v22 update added at the same time; switching components to `OnPush` is a behaviour change that deserves its own review. Done this way, a codebase can be modernised gradually while features keep shipping, and every step is small enough to revert.

  • Why run Angular's control-flow migration before the standalone migration?
    The standalone step adds each component's template dependencies to its imports. If templates still use *ngIf and *ngFor, it adds NgIf and NgForOf only for the control-flow step to remove them again. Migrating templates first means smaller, cleaner imports and one less round of churn.
  • When would you enable backwardsCompatibleConstructors in Angular's inject migration?
    When decorated classes inherit from other decorated classes. Removing constructor parameters can then cause compile errors in subclasses calling super(...), so the option keeps an extra constructor(...args: unknown[]) signature, at the cost of more code.
  • Should you remove the Eager the v22 update added while running these schematics?
    No, keep it as a separate change. The schematics are behaviour-preserving refactors; switching a component from Eager to OnPush changes when it refreshes and needs its own testing and review.

saying these in an interview costs you the question

  • ng update already ran the standalone and signal migrations for you.
  • Run every schematic on the whole repo in one commit to save time.
  • Narrowing --analysis-dir to the feature folder is the safe way to go folder by folder.
  • The standalone bootstrap mode should run first so the app starts standalone.
  • Each schematic's leftovers mean it failed and should be reverted.