Angular 17 has brought lots of new features. One of them is Signal. If you’re an Angular user, you surely know that zone.js as the underlying mechanism of change detection in Angular. It is simple, elegant, and sufficent in terms of performance in most of cases. The beauty of zone.js is that you can use any kind of data stucture to for state, you can use plain old javascript object, observable, now you have one more option, signal. To understand signal, I will review what we have. In this post, I will use the following application to demonstrate the different ways to represent state in Angular. The source code of the app is hosted at github

Imgur

Phase 1 - The baseline application

The application has three component, the root component, which is the app component , and two child components which is held inside the app component. The application is run the production mode, the purpose is to remove the detection of ExpressionChangedAfterItHasBeenCheckedError, so that is closer to the real case. The two child components have a button that can update the the time on the screen, one button updates on synchrounously, another button updates asyunchrounously.

The following is when the sync button is clicked.

Imgur

The following is when the async button is clicked.

Imgur

For the sync button, it updates the time first, then zone.js only triggered one change detection cycle because of the click event handler. For async button, zone.js triggers one cycle because of the click event handler, then trigger another cycle by the setTimeout callback. The change detection cycle start from the root component. This is because I use the default change strategy, the cycle will peform change detection for each component reursvely.

Phase 2 - Use OnPush change detection strategy.

The defualt change detection works very well until people find it can cause performance issue because each change detection cycle can cause change detection of thousands or tens of thousands of component. So to reduce number of components of affected, we can change the change detection strategy to OnPush. We call these components OnPush components.

When the sync button is clicked, we found the following, Imgur

When the async button is clicked, we found the following. Imgur

We still trigger same amount of the change detection cycles for each click. But the components affected are reduced to components on the event hierachy, the components which is not on the event hierachy are not affected.

However, this cause a problem in the async button. The setTimeout callback, still trigger a change detection cycle, which means the zone.js is still working fine. But the problem is that, cycle will not perform change detection for the async component, which is a OnPush component. The reason is that Angular does not know that the async component does not know that it is dirty? I, as the developer, have to tell angular that this button is dirty.

Phase 3 - ChangeDetectorRef to recure

Use the following code will mark the component to be dirty of time is changed in the setTimeout callback.

updateTime() {
  this.time = new Date();
  console.log(
    'async time is updated to ',
    this.date.transform(this.time, 'mediumTime')
  );
  this.cd.markForCheck();
}

Now the asyn button will generate the following output.

Imgur

We can see that in the second cycle, change detection is peformed in the async button correctly.

This is good. But we put the responsibility of marking component dirty on the developer. In a sense, it is encapsulation leak. I as a developer have to know these ugly details angular change detection.

Phase 4 - Observable with AsyncPipe to mark dirty automatically.

To solve this encapsulation leak, Angular provide us AsyncPiple. The idea is that, instead of using plain old javascript to hold state, we use observable. In the template, we use AsyncPipe to subscribe the change of the observable, and mark the component dirty automatically, when observable emit. In this way, we no longer need to deal with ChangeDetectorRef directly.

 private subject = new BehaviorSubject(new Date());
  time$ = this.subject.asObservable();

  updateTime() {
    const time = new Date();
    this.subject.next(time);
    console.log(
      'async time is updated to ',
      this.date.transform(time, 'mediumTime')
    );
  }

  update() {
    setTimeout(() => this.updateTime(), 2000);
  }
<p>
  Time: {{ time$ | async | date : "mediumTime" }}
  <button (click)="update()">Update</button>
</p>

Phase 5 - signal, the reactive primitive

The Observable with AsyncPipe is an elegant solution. The developers do not need to explictly tell Angular that a component is dirty. Signal is similar to Observable/Subject in that when state change, some code can be executed.

In the following, the effect function can registered a section of code to be run when the signal used in side code change.

this.timeSignal = signal(new Date());
//
effect(() => {
  this.time = this.timeSignal();
  console.log(
    "async time is updated to ",
    this.date.transform(this.time, "mediumTime")
  );
  cd.markForCheck();
});


  updateTime() {
    this.timeSignal.set(new Date());
  }

  update() {
    setTimeout(() => this.updateTime(), 2000);
  }

This difference is that the effect function does not need to subscribe to the change of signal explicitly. In the effect callback, I explicitly tell angular the component is dirty. So in the next change detection cycle, angular will perform change detection for the component.

Angular can simplify the code above, by binding the signal function directly to the template, and the template function can tell angular the component is dirty, like the following.

  time = signal(new Date());

  updateTime() {
    this.time.set(new Date());
    console.log(
      'async time is updated to ',
      this.date.transform(this.time(), 'mediumTime')
    );
  }

  update() {
    setTimeout(() => this.updateTime(), 2000);
  }
<p>
  Time: {{ time() | date : "mediumTime" }}
  <button (click)="update()">Update</button>
</p>

Angular signal is a new reactivity primitive, the api is simple and easy to use, but I am not sure whether I should switch to use it as Observable still has lots feature that signal does have.