Angular’s Signal Revolution: Effortless Change Detection Explained — Unveiling the Inner Workings

Netanel Basal
Netanel Basal
Published in
3 min readAug 24, 2023

Angular’s signals bring a host of benefits to the table, one of which is their ability to seamlessly integrate with templates and “automate” change detection. This automation means that a component configured with the OnPush change detection strategy will be rechecked during the next change detection cycle, sparing developers from manually injecting ChangeDetectorRef and invoking markForCheck. To illustrate this advantage, consider the following example:

@Component({
selector: 'app-foo',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ num }}`,
})
export class FooComponent {
num = 1;
private cdr = inject(ChangeDetectorRef);

ngOnInit() {
setTimeout(() => {
this.num = 2;
this.cdr.markForCheck();
}, 3000);
}
}

Here, we explicitly inject ChangeDetectorRef and invoke markForCheck after updating the component's state. The new way with signals:

@Component({
selector: 'app-foo',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ num() }}`,
})
export class FooComponent {
num = signal(0)

ngOnInit() {
setTimeout(() => this.num.set(1), 3000);
}
}

In this improved version, we use a signal, and remarkably, the view still updates without needing to manually call markForCheck. In this article, we will delve into the inner workings of this feature, shedding light on how signals streamline change detection in Angular applications.

At a higher level of abstraction, you can conceptualize the current template as the consumer and every signal as the producer. When a signal undergoes a value change, it promptly notifies the template, which subsequently triggers a call to markForCheck.

Angular employs the ReactiveLViewConsumer class, an extension of the ReactiveNode class, to facilitate its reactive architecture. In this architecture, each reactive node corresponds to a node within the reactive graph. These nodes can play various roles, acting as producers of reactive values, consumers of other reactive values, or fulfilling both roles simultaneously.

In our specific context, the ReactiveLViewConsumer serves as a consumer for the signals we utilize within our templates.

When Angular executes the component template function, its initial step involves obtaining a reference to the reactive view consumer:

export function executeTemplate<T>(
tView: TView, lView: LView<T>,
templateFn: ComponentTemplate<T>,
rf: RenderFlags,
context: T) {
const consumer = getReactiveLViewConsumer(lView, REACTIVE_TEMPLATE_CONSUMER);
...
}

The getReactiveLViewConsumer function simply retrieves the current instance of the reactive view consumer from a designated slot in the LView or create a new instance if it doesn’t exists.

Next, it proceeds to invoke the runInContext function within the ReactiveLViewConsumer class. This function is responsible for orchestrating the following tasks:

class ReactiveLViewConsumer {
runInContext(
fn: HostBindingsFunction<unknown> | ComponentTemplate<unknown>,
rf: RenderFlags,
ctx: unknown,
): void {
const prevConsumer = setActiveConsumer(this);
this.trackingVersion++;
try {
fn(rf, ctx);
} finally {
setActiveConsumer(prevConsumer);
}
}
}

It temporarily sets the current consumer to the current instance of ReactiveLViewConsumer, ensuring that any reactive values accessed during the template execution function are attributed to this consumer.

In simpler terms, what this signifies is that our reactive view consumer has effectively noted and registered each signal that is employed within the template. This happens when we access the signal as you can see here:

class WritableSignalImpl<T> extends ReactiveNode { 
// ...

signal(): T {
this.producerAccessed();
return this.value;
}
}

The producerAccessed method is invoked, marking the current signal as a producer dependency for our reactive view consumer.

Moving forward, when the signal undergoes a value update, it invokes the producerMayHaveChanged method, which subsequently triggers the onConsumerDependencyMayHaveChanged method for each consumer that has registered for this producer.

In our specific context, the ReactiveLViewConsumer class overrides this method, and when called, it in turn initiates a markViewDirty operation for the current view.

Lastly, the process concludes with the invocation of the commitLViewConsumerIfHasProducers function. This function serves the purpose of preserving the current reactive view consumer instance, but only if there are producers within the template.

Follow me on Medium or Twitter to read more about Angular and JS!

--

--

Netanel Basal
Netanel Basal

Written by Netanel Basal

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.

Responses (3)