Mert Özen Further With Every Line
Frontend Angular — #20

What Angular 13 Brought: Upgrading with ng update, Ivy-Only Angular and the New Dynamic Component API

Mert Özen Oct 31, 2026 9 min 2 views
What Angular 13 Brought: Upgrading with ng update, Ivy-Only Angular and the New Dynamic Component API

We upgrade from Angular 12 to 13 step by step with ng update. View Engine's removal, the end of IE11, factory-less createComponent, the persistent build cache and Node/TypeScript requirements.

Version note: This post covers what Angular 13 introduced; the code targets Angular 13.

What Happened After Angular 12?

So far we have built everything on Angular 12: NgModules, constructor injection, class-based guards, *ngIf and *ngFor. That foundation still holds, and as you will see, Angular did not throw it away overnight. But the framework did not stand still either. For years a new major version came out every six months, and each one simplified things a little more.

From this post on, we will follow Angular version by version. Each time we will first say "this is how we did it in Angular 12", then look at how the same job is done in the new version. The first stop is Angular 13, released on November 3, 2021. Before we get to the new features, let's meet the tool that moves us from one version to the next: ng update.

What Does ng update Actually Do?

Upgrading an Angular project is not just bumping version numbers in package.json and running npm install. In the new version an API signature may have changed, a setting may be gone, or a config file may need a new line. Finding and fixing all of that by hand takes hours.

ng update handles it in two steps. First it pulls the Angular packages up to the target version and updates package.json. Then it runs the migrations that ship with those packages. A migration is a small script that reads your code and adapts it to the new version; the Angular team writes one for most of the things they change or remove. Think of a good moving company: it does not just carry the furniture to the new house, it also rewires the lamps to fit the new outlets.

Know one rule up front: ng update moves only one major version at a time. Try to jump straight from 12 to 15 and the CLI stops with a message like this:

Updating multiple major versions of '@angular/core' at once is not supported. Please migrate each major version individually.

The reason is simple. Each version's migrations expect the code shape of the previous version; 14's migrations cannot do their job before 13's have run. The path is always the same: 12 to 13, 13 to 14, then on to 15. No taking the stairs two at a time.

From 12 to 13, Step by Step

First, get the project into a clean git state. An upgrade can touch dozens of files, and if something goes wrong you need a solid point to return to:

git status
git add .
git commit -m "chore: state before Angular 13 update"

Even if you skip this, ng update will stop you; it refuses to run while there are uncommitted changes. There is an escape hatch called --allow-dirty, but I would not use it. Afterwards you cannot tell which change came from the upgrade and which came from you.

Second, the Node version. Earlier in the series we installed Node 16 with nvm. Node 16 was not on Angular 12's official list but worked fine in practice; with Angular 13 it becomes officially supported. Still, make sure your terminal is on the right version:

nvm use 16
node -v

The output should be v16.10 or higher. If you put a .nvmrc file containing just 16 in the project root, a plain nvm use in that folder is enough. When later versions raise the Node requirement, we will switch the same way with nvm install; same coffee machine, different capsule.

Now let's see what can be updated:

ng update

Without arguments, ng update changes nothing. It scans the Angular packages in the project and lists the ones that can be upgraded, together with the command you need to run. The actual upgrade is done with this command:

ng update @angular/core@13 @angular/cli@13

Writing the version explicitly as @13 matters. Leave it out and ng update targets the latest version, which runs straight into the "multiple major versions" error above. While the command runs, you will see lines showing the packages being updated and then the migrations running one by one. If your project has other Angular-dependent packages such as Angular Material, add them to the same command, for example @angular/material@13.

When the migrations finish, check three things: does the app start with ng serve, does ng build complete without errors, is ng test green. If all is well, look over the changes with git diff and commit. That commit becomes the starting point for the next upgrade. Before every major upgrade it is also worth checking the interactive guide at angular.dev/update-guide; pick the versions you are moving between and it lists the manual steps you need.

Node and TypeScript Requirements for Angular 13

Angular 13's official Node range is 12.20 or later, 14.15 or later, or 16.10 or later. On the TypeScript side, Angular 13.0 needs at least TypeScript 4.4; later 13.x releases support up to 4.6. An Angular 12 project comes with TypeScript 4.3, so TypeScript has to move up a step here as well.

In most cases ng update also pulls TypeScript up to a compatible version. Even so, take a look at the typescript line in package.json after the upgrade. If the version does not fit, the Angular compiler fails before it starts, with an error telling you which range it expects.

One more note on RxJS: Angular 13 officially supports RxJS 7. An Angular 12 project comes with RxJS 6.6, and you can keep using 6.x. Moving to RxJS 7 is not required; it is a separate task you can take on when you like.

View Engine Is History

Inside Angular there is a compiler and rendering engine that turns your templates into JavaScript the browser can run. For a long time that job was done by an engine called View Engine. In Angular 9 Ivy became the default engine, in Angular 12 View Engine was deprecated, and in Angular 13 it was removed completely. Angular now runs on Ivy only.

Picture a car with two engines. For years the old engine was kept under the hood as a spare, because some parts were still built for it. Angular 13 pulled that old engine out. The team no longer has to keep two engines running side by side, and many of the features we will see in later versions became possible because of it.

As an application developer, not much changes in your day-to-day work; you were already using Ivy in Angular 12. The change mostly affects library authors. Angular libraries are now published in a format called "partial Ivy", and the CLI migration switches your own library projects to that mode. If you still depend on a library published in the old View Engine format, a compatibility tool called ngcc converts it to Ivy for now. That bridge will be removed later too, so this is a good time to review your dependencies.

Goodbye, Internet Explorer 11

Angular 12 had marked IE11 support as deprecated. Angular 13 dropped it entirely. Your first reaction might be "nobody uses IE anyway", but this decision has a direct effect on the build output.

To support older browsers, the CLI could produce both a modern and an older ES5 version of the same app, using a technique called differential loading. CLI 13 removed differential loading; there is now a single bundle for modern browsers. A migration that runs during ng update also cleans out the polyfills in your polyfills.ts that were only needed for Internet Explorer.

If your client still requires IE11, the answer is clear: that project has to stay on Angular 12. Having that conversation before you start the upgrade is much cheaper than discovering it halfway through.

Creating Components Dynamically Got Shorter

Sometimes, instead of writing a component into a template up front, you want to create it at runtime from code. A notification box, a modal, a widget that changes based on what the user picks. Let's say we have a simple alert component:

@Component({
  selector: 'app-alert',
  template: `<div class="alert">{{ message }}</div>`
})
export class AlertComponent {
  message = '';
}

In Angular 12, to put it on the screen dynamically, you first had to produce a factory:

import { Component, ComponentFactoryResolver, ViewChild, ViewContainerRef } from '@angular/core';
import { AlertComponent } from './alert/alert.component';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="showAlert()">Kaydet</button>
    <ng-container #host></ng-container>
  `
})
export class AppComponent {
  @ViewChild('host', { read: ViewContainerRef }) host!: ViewContainerRef;

  constructor(private resolver: ComponentFactoryResolver) { }

  showAlert(): void {
    this.host.clear();
    const factory = this.resolver.resolveComponentFactory(AlertComponent);
    const ref = this.host.createComponent(factory);
    ref.instance.message = 'Kayıt başarılı';
  }
}

With @ViewChild we reach the ng-container in the template as a ViewContainerRef; that is the slot the new component will sit in. Then we use the ComponentFactoryResolver from the constructor to resolve a factory for the component, and create the component with that factory. ref.instance is the created component itself, so we can set its properties directly.

In Angular 13 the same job looks like this:

export class AppComponent {
  @ViewChild('host', { read: ViewContainerRef }) host!: ViewContainerRef;

  showAlert(): void {
    this.host.clear();
    const ref = this.host.createComponent(AlertComponent);
    ref.instance.message = 'Kayıt başarılı';
  }
}

No factory, no resolver, no constructor. createComponent now accepts the component class directly. This is also tied to View Engine's departure: factories were something the old engine needed, while Ivy already carries everything it needs on the component class itself. The old factory-based signature was deprecated in Angular 13.0, and the ComponentFactoryResolver class followed in 13.2.

AlertComponent itself sits in AppModule's declarations list like any other component; the NgModule rules did not change in this version. If you come across an entryComponents array in older code, you can safely delete it. It had already become unnecessary with Ivy.

Why Builds Got Faster: The Persistent Cache

You will most likely notice one CLI 13 change on your second ng serve: startup is clearly faster. The reason is the .angular/cache folder that appears in the project root. During a build, the CLI writes intermediate results there, and on the next build it reads the unchanged parts from the cache instead of producing them again. It is like making dough ahead of time and putting it in the freezer; no need to knead from scratch every evening.

The default settings are not written in angular.json, but they are equivalent to this:

"cli": {
  "cache": {
    "enabled": true,
    "environment": "local",
    "path": ".angular/cache"
  }
}

The "local" value means the cache is on only on your own machine and stays off in CI. The CLI looks at the CI environment variable to figure out where it is running. If you want the cache in CI too, set the value to "all"; to turn it off entirely, ng config cli.cache.enabled false is enough.

This folder should not go into git. A migration that runs during the upgrade adds the .angular/cache line to your .gitignore by itself.

A Quiet Change in Testing

There is also something new on the TestBed side we saw earlier. In Angular 13, the test module and the components it created are destroyed after each test by default. The setting is called destroyAfterEach. The benefit: DOM leftovers or open subscriptions from one test do not leak into the next, and every test starts with a clean page.

But if this behavior were suddenly switched on in existing projects, tests that quietly lean on each other could break. So the migration deliberately keeps it off in existing projects. After the upgrade you will see something like this in src/test.ts:

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting(),
  { teardown: { destroyAfterEach: false } }
);

My advice: at a convenient moment, flip it to true and run ng test. Any tests that turn red were already relying on something a previous test left behind. Fixing them pays off in the long run.

A Small Experiment

Take any project you created with Angular 12; a practice project you opened with ng new in earlier posts will do. Commit everything first, confirm with node -v that you are on Node 16, and run ng update @angular/core@13 @angular/cli@13.

Then use git diff to look for these: the .angular/cache line added to .gitignore, the teardown setting in test.ts, and the bumped TypeScript version in package.json. Next, add the AlertComponent example above to the project; write it the Angular 12 way with a factory first, then convert it to the factory-less version. Finally, start ng serve twice and see how long the second startup takes.

This upgrade mostly cleaned things up under the hood. The changes in the next version are more visible: Angular 14 brings standalone components, the first take on components without NgModules, long-awaited type safety for reactive forms, and the inject() function as an alternative to the constructor.