Introduction to Angular Signals

Vlad Sharikov
AngularWave

--

Have you heard about Angular’s latest killer feature — Signals? Are you wondering how this new toy can help you build faster, cleaner apps, or whether it’s all just smoke and mirrors? If you’re like me — a code junkie who loves getting into the nitty-gritty details of how things work — you’ve probably also noticed a lack of deep dives into how Signals work.

That’s why I decided to pull back the curtain on this much-talked-about feature. After diving into source codes, dissecting examples, and immersing myself in articles and videos, I’ve discovered some insights that go beyond the usual API tutorials.

So, why should you stick around?

Topics I am going to cover in that article cycle

  1. The introduction, the explanation & examples of signals. What are they, and what are their properties?
  2. Why Signals are needed in Angular?
  3. Learn whether it’s finally time to say goodbye to RxJS — and how to do it.
  4. What is a high-level conception of signals, and what is the public API?
  5. What are Internal structures & their working principles, and what happens after you use the API? I will cover it based on the examples with an explanation of how fine-grained reactivity properties are achieved.
  6. Discover why zone.js could be holding you back and how to get rid of it for good.
  7. How are Signals used in Angular, and how does it work right now? Interoperability with the RxJS.

Ready to become an Angular Signals pro? Let’s dive in!

Making data reactivity in Angular easier

I love the Angular way of doing things, and I like reactivity. There is RxJS that is highly integrated into Angular. I think RxJS is a very powerful tool to combine your data, events and user interaction, but frankly speaking, it has a lot of pitfalls and misunderstandings and quite a steep learning curve. Looking ahead: No, Angular Signals is not a replacement for an RxJS, but it is a great new tool to handle data reactivity in your app, and it will help a lot with data reactivity. Signals handle data reactivity way simpler. The learning curve, I suppose, is amazingly gentle. Also, it can have seamless integration into the framework. Looking ahead, yes, you will be able not to use the AsyncPipe or manual markForCheck call. You usually use those in your Angular applications right now if you use OnPush optimization.

What are Angular Signals?

Angular Docs states:

Angular Signals is a system that granularly tracks how and where your state is used throughout an application. It allows the framework to optimize rendering updates.
A signal is a wrapper around a value that can notify interested consumers when that value changes. Signals can contain any value, from simple primitives to complex data structures.
Signals may either be writable or read-only.

In my words, a signal is a value that can notify others about changes in its state. That is possible because the signal is a value wrapped in a getter. Every time you want to get (or pull) the value, you call a function, and in that call, some additional logic can be applied to modify & maintain internal structures. What does this mean in everyday practice?

Signals in a few examples

Writable signal:

const something = signal('one');
something(); // outputs: 'one'
something.set('two');
something(); // outputs: 'two'

This is an example of a writable signal. The something signal is created with an initial value; then the value is pulled; then a new value is pushed, and then a value is pulled again. Here, you can see the most basic public API usage. The signal function returns another function, which has to be called to get the value. It also has a set of methods that allow you to modify the state, like set, mutate or update. The function call to get or pull the value is crucial, and it is necessary to add additional logic to maintain internal structures under the hood correctly.

Computed signal:

const something = signal('one');
const computedThing = computed(() => `${something()} thing`);
computedThing(); // outputs: 'one thing'
something.set('another');
computedThing(); // outputs: 'another thing'

This is a computed signal. Here is the same something signal, but after that, a new computed signal is used. A computed signal is able to use other signals as dependencies, i.e., it derives value from other signals. The something signal is a dependency of computedThingsignal. In the 3rd line, you will get the one thing, and in the 5th line, you will get another thing. You can not update the value of the computed signal explicitly. You only update signals on which it depends, and derived signals will be re-computed.

Signal in Angular:

@Component({
selector: 'my-app',
standalone: true,
template: `
Counter: {{counter()}}
<button (click)="inc()">Inc</button>
`,
})
export class App {
counter = signal(0);
inc() {
this.counter.set(this.counter() + 1);
}
}

Here, you see an Angular simple signal usage. To get or pull the value, you still need to invoke a function, and you actually do it in the template. In that example, the counter value is shown, and there is a button to increment the value. When you click it, you will see the counter is increased.

Also, you will have a new effect function. There may be cases when you need to run code when a signal changes and you want to run a side effect like thesetInterval call, for example.

Properties of signals

Let’s check a list of fine-grain reactivity properties. The Angular Signals system implements that concept, so it should have those properties.

  1. Reactive value handling. Value is reactive, which means when it is changed, it will notify its consumers.
  2. Writable signals. Signals can be writable, allowing you to change the state of a signal and notify its consumers.
  3. Derived signals. Signals can depend on other writable signals or even other derived signals.
  4. Automatic dependency graph and dynamic dependency tracking. One signal can depend on another or other signals. You write the code and don’t need anything like specifying an array of dependencies to track. Also, dependencies will be recalculated automatically on each access, so everything will be kept fast and efficient. Also, the dirtiness of the dependency is counted, too, so there will be no unneeded updates. We will get to those cases later.
  5. Side effects. There is a possibility to run functions with side effects inside a reactive context.
  6. Glitch-free execution. You will not have a situation when the received state is inconsistent. Because of tracking all dependencies, everything will be recalculated and up to date in any case.

In the following articles, when analyzing the internal structure of the Angular implementation, we will touch on these properties.

Glitch-free nature

What is the glitch-free property of signals? You can read more about it here. In the case of signals, we will always have consistent execution without intermediary states. That happens because they track their dependencies, and the update mechanism has two phases. While changing a signal, you notify its dependencies about changes, marking all of them as stale, which is the first phase. After that, you access some derived signal, which depends on the changed signal, causing actual re-calculations, which is the second phase. I will cover that in detail in the following article.

Let’s discuss that using this comparison with RxJS. Consider two examples. The first one uses RxJS:

export class MyComponent {
value: BehaviorSubject<number> = new BehaviorSubject(this.val);
mapped: Observable<number> = this.value.pipe(map((v) => v + 1));
val = 0;

constructor() {
combineLatest([this.value, this.mapped])
.pipe(
map(([signalValue, derivedValue]) => `Current state. signal: ${signalValue}, derived: ${derivedValue}`)
)
.subscribe(console.log);
}
onClick() {
this.value.next(++this.val);
}
}

Another one uses signals:

export class MyComponent {
signal: WritableSignal<number>;
derived: Signal<number>;
val = 0;

constructor() {
this.signal = signal(this.val);
this.derived = computed(() => this.signal() + 1);
effect(() => {
console.log(`Current state. signal: ${this.signal()}, derived: ${this.derived()}`);
})
}
onClick() {
this.signal.set(++this.val);
}
}

Those examples are almost the same. In the end, on click, they produce a log. What will happen in those two cases? In the case of RxJS implementation, you will actually have two logs: one for the intermediate state and one for the final state. In signals, you have one log, which represents the consistent state. I actually remember the problem with RxJS in my projects and how I was trying to solve it & I was discussing it with colleagues. That was a really huge pain. After a long time, I found that that problem is quite famous, and it even has a name: diamond problem.

Will Angular Signals replace the RxJS?

Frankly speaking, these two are different things. RxJS is a different and very powerful thing that solves other problems. Signals are about the data and the state. The RxJS, I assume, is a broader thing. It is about data, too, but at first, it is more about streams of data, events, user interactions and others.

Moreover, a lot of applications use RxJS right now for state management. It is even possible to use helpers like toObservable and toSignal from the @angular/core/rxjs-interop package.

You might think that RxJS allows the same things, but still, they are different. They are focused on different things. In RxJS, everything is built around streams that go through pipelines that transform, filter & combine them. Signals are built around pieces of data.

As I mentioned at the start of the article, I don’t think that signals are here to replace the RxJS.

Let’s have two lists to clarify the difference:

Why do we need it in Angular?

A mechanism that synchronizes the data model and the UI is a crucial part of any modern UI framework. Right now Angular relies on the zone.js. When something asynchronous happens, the zone.js tracks that and gives the framework information that something might be changed. Now, the framework is able to compare previous and new states in the whole application, perform global change detection, and sync the UI.

There is a concept called fine-grained reactivity. It is when you can clearly understand what smallest atomic part of the state was changed. That kind of reactivity is well used in other frameworks like Vue, Solid, Preact, and Svelte (it is even hidden behind the compiler in that case), and this concept is great here. It is not a new thing. It is used in other frameworks too. That does not happen with the current approach with the zone.js. Although there is an optimization with using OnPush components but that is a different thing.

In the case of Angular with signals, it is achievable to have local change detection of a component. In the case of zone.js, after certain moments, the framework has to compare a lot of data in the whole application to understand what the changes are. In the concept of signal, it receives the information about changing a piece of state very granularly. Hence, it can understand that an update was performed and start the change detection. Moreover, it knows what exactly was updated, does not perform global application change detection, and is able to make it locally. Great, huh?

Concepts and internal structures of Angular’s fine-grained reactivity system

For now, you know the properties of signals as reactive primitives. You know signals can depend on each other. You know that the value here should somehow notify its customers about changing its state. So there should be some connections. Through those, signals will be connected with each other to communicate somehow. Also, the reactivity system builds connections automatically, and after that, it can keep only needed connections and cut unnecessary ones.

Here is a basic example of a reactivity graph or dependency graph:

You can see that 2nd node depends on 1st. The 3rd node depends on the 1st and the 2nd. The 4th node depends on the 1st, the 3rd, and so on. You can understand that by looking at the connections between nodes.

The Angular team introduces two abstractions: Producer and Consumer. Producers are values that can deliver notifications about changes, and consumers consume them. Actually, there is a case when one node can be both producer & consumer. That happens for a computed signal, which is used as a dependency for another computed signal. Only computed or derived signals can be producers & consumers at the same time. Writeable signals can only be producers. They can’t consume any dependencies.

Angular Signals is an implementation of a fine-grained reactivity system. The core things in the Angular Signals system are the ReactiveNode and ReactiveEdge classes. When you use signals public APIs, those structures are actually created and maintained under the hood.

All discussed class members that I cover in the current section are private or protected, and they are not part of the public API. The Public API is signal, computed, effect functions and set, update, and mutate methods on the function returned by the signal function call.

Pseudocode to show the internal structure:

abstract class ReactiveNode {
protected consumers: Map<string, ReactiveEdge>;
protected producers: Map<string, ReactiveEdge>;
protected trackingVersion: number;
protected valueVersion: number;
}

The ReactiveNode is a base abstract class; it is not instantiated. It is needed for concrete implementations: ComputedImpl , WritableSignalImpl and ReactiveLViewConsumer. Let’s talk about the first two implementations. The WritableSignalImpl can only be a producer, and it is instantiated while you are using the signal function or set/update/mutate methods. The ComputedImpl is instantiated when you use the computed function. The ReactiveNode abstract class provides a common implementation for all types of derived classes and ensures that derived classes implement their methods. It contains properties to store the state of the reactivity nodes like consumers and producers maps. That class also contains the logic both for building connections between the reactivity nodes and removing them to keep the dependency graph up to date. Basically, it maintains the state of consumers and producers maps of the node.

Methods that must be implemented by derived classes are:

/**
* Called for consumers whenever one of their dependencies notifies that it might have a new
* value.
*/
protected abstract onConsumerDependencyMayHaveChanged(): void;

/**
* Called for producers when a dependent consumer is checking if the producer's value has actually
* changed.
*/
protected abstract onProducerUpdateValueVersion(): void;

Concrete implementations should implement these methods. The WritableSignalImpl does not do it because it can only be a producer. The ComputedImpl implements both of them because the computed signal can be both producer & consumer, so it should do something with the reactivity node.

class WritableSignalImpl<T> extends ReactiveNode {
private value: T;
private equal: IValueEqualityFn<T>;
}

class ComputedImpl<T> extends ReactiveNode {
private value: T;
private stale: boolean;
private equal: IValueEqualityFn<T>;
}

The ReactiveEdge has weak references to nodes it is connecting, seenValueVersion and atTrackingVersion properties.

interface ReactiveEdge {
readonly producerNode: WeakRef<IReactiveNode>
readonly consumerNode: WeakRef<IReactiveNode>;
atTrackingVersion: number;
seenValueVersion: number;
}

These are internal structures that the Angular framework uses to build the reactivity system. In the following article, let’s explore how those properties help it to achieve properties that fine-grained reactivity systems should have.

Conclusion

That is an introduction to Angular Signals. There is a lot more to say, and in the following articles, let’s have a deeper dive into the subject. I am going to talk about how exactly it works in Angular and why markForCheck is not needed, how exactly the dependency graph is built under the public API, and how is it maintained. Stay tuned.

--

--