Stop extracting state from observables!

Here’s a bit of code I see far too often:

export class UserProfileComponent {
  firstName: string;
  lastName: string;
  zipCode: string;
  
  ngOnInit() {
    http.get('/user/profile').subscribe(profile => {
      this.firstName = profile.firstName;
      this.lastName = profile.lastName;
      this.zipCode = profile.zipCode;
    });
  }
}

I call this “extracting state from observables.”

Why it sucks

Issues with ChangeDetectionStrategy.OnPush

It’s a common performance trick to set ChangeDetectionStrategy to OnPush. Angular no longer checks components for changes, instead the developer must call ChangeDetectorRef.markForCheck() to ensure the DOM is updated when component state changes. This causes bugs. Most frequently I see devs:

Race conditions

Continuing from the above example, suppose our UserProfileComponent (or another component on the page) allows the user to change their profile information. Once an update is complete, we want to prompt the user via snackbar, showing them their new profile data. We may write something like:

onProfileUpdate(newProfileData) {
  http.post('/profile', newProfileData)
    .subscribe(updatedProfile => {
       this.firstName = updatedProfile.firstName;
       // etc.
    })
    
  snackBar.open(
     `Successfully updated user data to First name: ${this.firstName}, ${this.lastName}, ${this.zipCode}`
  );
}

It is impossible from this code alone to know what will be shown in the snackBar. It could render with the newly updated first name, last name, and zip code or it could render with the old names - depending on how the source observable works.

Cognitive Complexity

When we write like this, the more state inside a component the harder it gets to read. Not only that, but the harder it gets to be certain of how all the state interacts. Fully understanding where properties are written from and ensuring writes and reads happen in the right order can become difficult for even short components. Instead, let’s see how we can simplify:

Refactoring

export class UserProfileComponent {
  private profile$ = http.get('/user/profile').pipe(shareReplay(1));
  firstName = this.profile$.pipe(map(p => p.firstName));
  lastName = this.profile$.pipe(map(p => p.lastName));
  zipCode = this.profile$.pipe(map(p => p.zipCode));
}

Or, preferably, with Signals (available in Angular 16+):

export class UserProfileComponent {
  private profile = toSignal(http.get('/user/profile'));
  firstName = computed(() => this.profile().firstName);
  lastName = computed(() => this.profile().lastName);
  zipCode = computed(() => this.profile().zipCode);
}

How the refactoring helps

AsyncPipe calls markForCheck for us. Signals are built into the core of the framework, so they will also take care of that for us.

Signals and observables, by their definition, also handle control flow; derived properties are updated automatically. Writing this way can be hard for new developers initially, but the reward is worth it. Developers don’t need to call functions in the “right” order or track who reads what and where. The data structure handles this for you.

Because of the tight coupling between source observables/signals and their children, it’s easy to trace where and when a field will change.