In this post I will take a look at the new downgradeModule in ngUpgrade.

Some of you may already be familiar with ngUpgrade and upgradeModule.

UpgradeModule works great, but we now have an alternative called downgradeModule.


The main difference between upgradeModule and downgradeModule is change detection management.

UpgradeModule runs AngularJS and Angular in the same Angular zone. This causes a tight coupling between change detection in AngularJS and Angular.

This means anything that causes a change detection cycle in Angular will also trigger change detection in AngularJS.

Examples of this are setInterval and setTimeout.

In regular AngularJS applications, setInterval and setTimeout won’t run a digest cycle unless you manually trigger it via scope.$digest or similar. Instead we would normally use $interval and $timeout if we need change detection in combination with timers.

Introducing ngUpgrade with upgradeModule in an existing AngularJS application changes this behavior.

Since ngUpgrade bootstraps the application with zones, events like xhr, setInterval and setTimeout are patched to trigger change detection. As a result we will trigger digest cycles where we previously didn’t.

In some cases this will be convenient since we no longer have to worry about wrappers like $interval and $timeout.

However, I find that people often used setTimeout/setInterval in AngularJS, specifically to opt out of change detection. It could be that we needed some sort of polling mechanism, but wanted to avoid a potentially costly digest cycle for every timer tick.

One way to reverse this behavior is to run timers in a new Zone, but that means we have to find all timers and wrap them in a new Zone.


One of the features of downgradeModule is more separation between the change detection cycles of AngularJS and Angular. This is achieved by bootstrapping AngularJS outside the Angular zone.

Basically this gives us a way to escape zone.js’ patching of common JavaScript events like xhr, setTimeout, etc.

Bootstrapping the application with downgradeModule is a little different than the upgradeModule counterpart:

import { platformBrowser } from '@angular/platform-browser'; import { downgradeModule, downgradeComponent } from '@angular/upgrade/static'; import { StaticProvider } from '@angular/core'; import './angularjs-app/app'; import { AppModuleNgFactory } from './app/app.module.ngfactory'; import { TextEditor } from './app/text-editor/text-editor'; import { Increment } from './app/increment/increment'; import { IncrementService } from './app/increment/increment.service'; declare var angular: any; const bootstrapFn = (extraProviders: StaticProvider[]) => { const platformRef = platformBrowser(extraProviders); return platformRef.bootstrapModuleFactory(AppModuleNgFactory); }; const downgradedModule = downgradeModule(bootstrapFn); angular.module('ng-upgrade', [ 'old-angularjs-app', downgradedModule ]); angular.module('ng-upgrade').directive('editor', downgradeComponent({component: TextEditor})); angular.module('ng-upgrade').directive('increment', downgradeComponent({component: Increment})); angular.bootstrap(document, ['ng-upgrade']);

The biggest difference is in the bootstrapping.

Instead of chaining two bootstrap calls, the Angular module is passed in as a downgraded module to the AngularJS bootstrap call.

The good news is that downgrading of components work the same way as before. There is however a change to downgrading injectables.

From what I can tell, we can no longer downgrade Angular services and inject them in standalone AngularJS code.

With upgradeModule this works seamlessly, so we are giving up a bit of functionality with this. My biggest concern is that it means giving up services as a way to share state between Angular and AngularJS. It may also slow down the momentum of upgrading services.

Good news is that we can still pass state using EventEmitters though.