← Back to blog Post

How to upgrade to Angular 6 without dying in the attempt

Hosted here as an English translation. Originally published on Medium.

Keeping an application up to date is one of the most important things we can do for security, performance, and long-term maintainability. The problem is that a new major version can easily turn that sensible maintenance task into a painful migration.

That was exactly the case with Angular 6. The new version introduced architectural changes that are easy enough to live with in a brand new app, but much more awkward when you are moving an existing codebase.

Fortunately, the Angular team provided a migration guide to help developers through the process:

Angular Update Guide

That guide covers the official Angular ecosystem well, but real applications rarely depend on Angular alone. They also use libraries such as NgRx for state management or AngularFire2 for Firebase, and those dependencies may fall outside the happy path.

The main source of friction in this migration is RxJS 6. The version jump changes the way imports and operators work, and that can affect both third-party packages and the application code itself.

Updating the framework version

The first step is to follow the official Angular upgrade strategy. The update guide asks which version you are coming from, which version you want to reach, and which tooling you use. From that, it generates a task list tailored to your case.

That is valuable because it does more than provide commands. It also explains why each step exists and links back to the relevant official documentation.

At the end of the migration, the guide often recommends removing rxjs-compat once the whole codebase has been updated.

What rxjs-compat is for

rxjs-compat exists to preserve compatibility with code written against the older RxJS style. If you keep it installed, a large part of the project may continue to work with fewer immediate changes.

That can be useful as a transitional step. It lets you upgrade the foundation of the app first and postpone the deeper code cleanup.

It also comes with a trade-off: you are carrying a compatibility layer that should not stay around forever. If the goal is a clean Angular 6 and RxJS 6 codebase, you eventually need to remove it and update the code properly.

NgRx

With NgRx, the upgrade is usually more straightforward because its packages are commonly included in the normal ng update flow.

For example:

ng update @ngrx/core

Depending on the project version, you may also need to review packages such as @ngrx/store, @ngrx/effects, or @ngrx/entity, but this part is normally less troublesome than the reactive code migration itself.

Firebase and AngularFire2

If your application uses Firebase, it probably also depends on firebase and angularfire2. Both packages were affected by the RxJS version transition, so it makes sense to update them together.

One practical approach is to move both to the latest compatible releases:

npm install firebase@next angularfire2@next

At the time, that was the quickest way to align them with Angular 6 and RxJS 6 without getting stuck between incompatible dependency ranges.

The real work: application code

So far, the process sounds manageable. The real difficulty starts when we adapt the code itself.

RxJS 6 changes import paths. Where older code often looked like this:

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

the new style centralizes imports in rxjs and rxjs/operators:

import { Observable, of } from 'rxjs';
import { catchError, map, mergeMap, switchMap, take } from 'rxjs/operators';

It is not just a cosmetic change. The way operators are chained also changes.

Before, it was common to write:

this.myObservable().map((data) => {
  // ...
});

With RxJS 6, operators go through pipe:

this.myObservable().pipe(
  map((data) => {
    // ...
  })
);

Some helpers and operators also change shape:

return Observable.of();

becomes:

return of();

And catch() becomes catchError():

catchError(() => of(fallbackValue))

A real example

An NgRx effect written with the older syntax could look like this:

getUser: Observable<Action> = this.actions.ofType(userActions.GET_USER)
  .map((action: userActions.GetUser) => action.payload)
  .switchMap(() => this.auth.user$)
  .map((authData) => {
    if (authData) {
      return new userActions.Authenticated({ ...authData });
    }

    return new userActions.NotAuthenticated();
  })
  .catch(() => Observable.of(new userActions.AuthError()));

After the migration, the same flow becomes:

getUser: Observable<Action> = this.actions.ofType(userActions.GET_USER).pipe(
  map((action: userActions.GetUser) => action.payload),
  switchMap(() => this.auth.user$),
  map((authData) => {
    if (authData) {
      return new userActions.Authenticated({ ...authData });
    }

    return new userActions.NotAuthenticated();
  }),
  catchError(() => of(new userActions.AuthError()))
);

That difference becomes very noticeable when the codebase has many reactive flows spread across services, effects, and components.

Conclusion

Angular provides good tooling for updating the framework version, but not every part of the migration can be automated. The delicate part is understanding the mental shift introduced by RxJS 6 and applying it carefully across the existing code.

Once that pattern is clear, the migration stops being mysterious and becomes mostly mechanical work: update imports, move operators into pipe, replace old helpers, and align ecosystem dependencies.

If you are in the middle of this upgrade, the most practical approach is:

  • use the official guide for the Angular core update,
  • upgrade dependencies such as NgRx and AngularFire2 together,
  • and only remove rxjs-compat once the RxJS migration is truly complete.

That keeps the migration safer and leaves the application in a much cleaner state for future changes.

  • angular
  • rxjs
  • firebase