New reactivity in Angular. Is it time to say “farewell” to RxJS?

Sergey Gultyayev
4 min readMay 9, 2023
Photo by Julian Hochgesang on Unsplash

On May 3rd Angular v16 has been released. This is one of the greatest releases that we have received in the recent years. In the release there is a thing called “signals”, but what is it? Why is there so much buzz around it?

Recap on current reactivity in Angular

Compared to React in Angular we don’t have any setState function that would update the state and tell the framework to re-render the template. Instead, we have a library called zone.js. It works by monkey-patching Browser APIs so that when any asynchronous event happens it knows about it. Thanks to that Angular is receiving notifications on when to start a new change detection cycle.

With that we get some drawbacks such as change detection running all over the application even in cases when we may not update the template bound values. Furthermore, it adds a lot of weight to the bundle size (about 100 KB).

Because of change detection running on any occurrence we have different change detection strategies: OnPush and Default.

And all of this is just a tip of the iceberg on how the change detection works in Angular. Even seasoned developers encounter quirky behaviours. For beginner ones it’s the Pandora’s box.

What are signals?

Signals are not a new concept in web development. However, Angular team has built them from the ground to ensure it fits best under the current needs and architecture.

In essence, a signal is a “magic function” which holds current value and has methods to update it. Thanks to that we also get functions such as effect and computed which execute only when a signal’s value changes. Moreover, you don’t need to provide any “deps” array like for React’s useEffect or useMemo hooks! The functions keep track of the used dependencies themselves!

Because of the fact that signals are built by Angular’s team — the framework will know when you use the signal in the template and its value changes! This way it will run change detection for that component (in future, now it will work similar to the async pipe as I understand). No need for Zones! Users will receive apps with better performance and developers will receive the framework with way simpler change detection mental model.

How it relates to RxJS

It’s an interesting topic in my opinion. The reason for that is that some developers on Twitter had an idea that signals are a simplified version of RxJS. In reality it’s not.

RxJS is a standalone library which provides us with great tools for reactive programming, whereas signals are a special state holding functions. Their purpose is to allow Angular to do the renders in a more optimised and granular way.

RxJS is not going anywhere. It’s a great library and signals won’t replace it. Nor were they meant to do so. However, since we use observables extensively we get functions which will transform a stream into a signal ( toObservable(<signal>) ) and vice versa ( toSignal(<observable>) )!

Moreover, when you convert your observable to a signal you don’t use the async pipe in the template. Also, you don’t need to manage the subscription manually. Angular will do it for you!

How to use signals

It’s really simple to start working with signals.

@Component({
template: `<p>{{ sayHello() }}</p>`
})
export class AppComponent {
name = signal("World")
sayHello = computed(() => `Hello ${this.name()}`)

update() {
this.name.set("John")
}
}

Here, we create a signal with initial value “World” and assign it to the property name . sayHello is a computed value which means that it will be updated each time name ‘s value changes. To read a signal’s value we have to call it ( sayHello() ).

We can update the signal’s value in three ways:

  1. Calling the set method which accepts a new value.
  2. Calling an update method this.name.update(oldName => oldName.toUpperCase()) .
  3. Calling a mutate method. As the name suggests you can mutate the value here safely.
todos = signal([])

addTodo(newTodo: string) {
this.todos.mutate(todos => {
todos.push(newTodo)
})
}

Signals in OnPush components (an excerpt from the docs)
When an OnPush component uses a signal's value in its template, Angular will track the signal as a dependency of that component. When that signal is updated, Angular automatically marks the component to ensure it gets updated the next time change detection runs. Refer to the Skipping component subtrees guide for more information about OnPush components.

What should happen next

It’s expected that soon we will receive Inputs and Outputs as signals. This means that we will be able to declare a signal input and derive a state from its value. No more boilerplate-ish solutions by using setters or ngOnChanges .

I believe that soon it should be declared that Zones are completely optional and can be replaced by signals.

In the ideal scenario signals would become functions that granularly update only parts of the template where the changes occurred, but I am not sure whether it’s achievable with the current architecture and how much performance it would give in return for making Angular even more complex internally.

While signals are in the developers preview they are stable. What may change is the API, but knowing the Angular team we will receive schematics for the smooth update. Given that I would say that we can start incorporating them in our production projects (those lucky ones that get upgrades to the latest versions :) ).

--

--

Sergey Gultyayev

A front-end developer who uses Angular as a main framework and loves it