Write More Reactive Code with Angular Signals

Angular Signals – a feature introduced in Angular 16 brings a new way of handling reactivity in applications. We discover what they are, how they work, how they interoperate with RxJS, and what revolutionary changes they bring.

The release of Angular v16 brought a very important feature many developers are already calling the framework’s renaissance – Angular Signals.

Personally, I wouldn’t say Angular ever fell out of favor, and NPM downloads don’t necessarily reflect if a framework is good or bad. However, one thing we can agree on is that it has a steeper learning curve compared to, say, React.

With Signals, this is bound to change because you no longer need complex patterns to start writing performant Angular code. Angular Signals make the platform more approachable to beginners, and in that sense, it does mark the beginning of a completely new phase.

Angular Signals bring reactivity in a fundamental change

The Angular team’s motivation for introducing this feature was adding fine-grained reactivity into the framework. This concept is fundamentally different from how Angular functions today, where zone.js is used to trigger global change detection for the whole application. The changes introduced will allow:

  • removal of zone.js at a certain point in the future (not imminent future; for now it is here to stay)
  • creating a clear model of how data flows through the application
  • built-in support for derived state
  • synchronizing the parts of the UI that need to be updated
  • better interoperability with other reactive libraries like RxJS. The Angular team announced a partnership with major state management libraries like NgRx, RxAngular, and many others in order to support interoperability
  • simplification of the entire framework

Why Angular Signals?

First of all, signals’ values are synchronous and always available. How many times did you want to get a simple value from an RxJS chain but had to go through all the hassle of subscribing and unsubscribing to read it? Here are some more upsides to using signals.

Local change detection.

Whereas zone.js triggered a global change detection check, a signal-based component will be scheduled for change detection check only when a signal read in the template notifies it that it has been changed. This allows for very precise updates.

Reading a value doesn’t trigger side effects.

Automatic dependencies tracking.

Angular Signals and consumers

Signals are a system that allows Angular to granularly track where a state is used and updated in the application, which then allows the framework to optimize the application’s rendering.

Angular Signals wrap a value that notifies all the consumers of a particular signal when this value is changed.

In signals, consumers are any part of the code that uses a signal’s value and wants to be notified about a change in that value. When a change occurs, the signal notifies all the consumers, which then act upon the change in the signal’s value.

A signal can contain any value. It can be either read-only or writable, and it can be called anywhere in the code. Let’s say you define a signal in a service. You can then use it in a template, component, pipe, or even other services. A signal value is reactive.

Where to find Angular Signals

Signals are available as of Angular version 16 as a developer preview, which allows you to experiment with them. Even though the majority of the API is stable and functional, this is a major change, so you can expect bugs, and the API is still subject to change.

We would recommend avoiding using Signals in production projects until they are 100% stable, and expect this to happen in Angular version 17. The developer preview only lists a basic subset of the API, while the inputs, outputs, etc., are announced for v17+.

Overview of Angular Signals syntax and usage

Creating a signal

You can create a new signal by calling a signal() function and passing in an argument that contains its initial value. Think of it as BehaviorSubject in the RxJS, but without the subscribe part.

	// Creates new signal 
const isActive = signal(false)

// Reads a signal
isActive()

Marking signal as read-only

The method asReadonly() will return a signal that is non-writeable. You can access its value, but it does not allow changing the signal.

	// Creates new signal
 const isActive = signal(false)

 // Returns read only version of isActive signal
 const readonlyIsActive = isActive.asReadonly()

Set a new signal value

To set or change the value of a signal programmatically, call the set() method.

	// Sets value of isActive signal to true
 isActive.set(true)

Update signal value based on the previous state

If a new state is derived from an old one, use the update() method where the first argument of the callback function is the previous state of the signal. You can then use it to calculate the new state.

	// Updates the state based on the previous value
 isActive.update(value => !value)

Mutating a signal

When working with arrays or objects, use the mutate() method to trigger updates. This is very useful because mutating a signal that contains an object will trigger change detection, whereas using a conventional approach wouldn’t because the object reference would remain the same. Before signals, you would either have to create a new array or object or call change detection manually.

	// Creates new signal
const items = signal({content: 'Item content'})

// Updates the property of an object
items.mutate(value => {
    value.content = 'Updated item content'
})

Computed signals

Computed signals derive their value based on one or more other signals. Whenever one of those signals changes, the computed signal also changes.

It’s important to note that computed values are cached, meaning that every future read of that computed signal will return cached values instead of recalculating them, until the signals computed depend on the changes. You can think of them as pure pipes where the transform() function won’t run until the value changes. Computed signals take a derivation function to calculate value, and it is not possible to programmatically set the value using set or mutate methods. The dependencies inside the computed signals are dynamic, which means if you have some expensive nested signal computations, only the ones read during computations will be executed.

There’s another important note here – anything derived from a signal is also a signal.

	// Creates new signal
const isActive = signal(false)

// Creates new signal based on isActive
const isActiveLabel = computed(() => 
   isActive() ? 'Active' : 'Inactive'
 )

Running side effects

The effect function is used for triggering side effects when one or more signals inside it change. Every effect function will run at least once, and every signal that has been called inside it becomes tracked. If any of those signals change, the effect function will execute again.

Use the effect function wisely and don’t use it to propagate state changes, as you can unintentionally introduce circular references or change detection cycles. An effect can only be called inside of the injection context, similar to the inject API. Meaning you cannot register an effect inside of a function call, for example.

Effects automatically destroy when the context they were created in (component/service) gets destroyed or they can be removed manually using the destroy method. Always make sure to destroy an effect, similar to how you would handle subscriptions in an app, or you risk memory leaks.

	// Creates new signal
const isActive = signal(false)
 
// Registers new effect which will run whenever isActive changes
effect(()=> {
    console.log(`The isActive signal changed value to ${isActive()}`)
 })

Interoperability with RxJS

The introduction of signals doesn’t mean that the Angular team is abandoning RxJS. In fact, they will make RxJS integration easier.

While signals are great for handling synchronous reactivity, they are not a good out-of-the-box solution for asynchronous reactivity (think API calls). The reason for this is that signals are synchronous and will set the value that came in last. For example, since the time of API calls varies, it may happen that a wrong value is set. However, you can transform an observable into a signal by calling toSignal() or vice versa by calling toObservable().

toSignal

The toSignal function internally subscribes to the given Observable and updates the returned signal any time the Observable emits a value. This subscription is created immediately as opposed to waiting until the signal is read.

The signature of the toSignal function supports both synchronous and asynchronous Observables. Angular Signals always need to have an initial value. In case one is not provided, the signal value remains undefined until an Observable emits the first value. This is useful if the Observable turned to signal is, for example, an API call.

The unsubscribe part is the same as the effect when the context in which the signal has been created is destroyed.

	// Creates an source observable
 const isActive$ = of(true).pipe(delay(5000))
 
// Transforms an observable to a signal with initial value of false
// The value of signal will be set to true after 5 seconds
 const isActive = toSignal(isActive$, {initialValue: false})

toObservable

toObservable is a function that takes an Angular Signals and returns an Observable. It does this by creating an effect under the hood when the Observable is subscribed to. This effect takes values from the signal and streams them to subscribers. All values are asynchronous. The function toObservable must be called in an injection context.

	// Creates new signal
const isActive = signal(true)
 
// Transforms a signal to an observable
const isActive$ = toObservable(isActive)

Function calls in a template

Over the years, Angular developers have learned to avoid calling functions inside templates because a function re-runs every change detection and used pure pipes instead. This would cause expensive computations to run multiple times unnecessarily if the passed arguments did not change.

In a signal-based component, this idea no longer applies because the expressions will only re-evaluate as a result of a signal dependency change.

With signals, we no longer have to care about handling subscriptions. It is absolutely fine to call a signal function in the template since only the part that depends on that signal will be updated.

	// example.component.ts
// Creates new signal
const isActive = signal(false)
 
// Creates new signal based on isActive
const isActiveLabel = computed(() => 
     isActive() ? 'Active' : 'Inactive'
 )
 
// example.component.html
// isActiveLabel will only run when the signal changes
<p>
  {{ isActiveLabel() }}
</p>

Proposed Signal APIs not yet available in the developer preview

During the signals’ RFC phase, the Angular team proposed a set of new APIs. These are not available in the developer preview of Angular version 16 but should become so in future versions. Since those APIs are still in the works, only basic descriptions are given.

Declare a component as signal-based

To mark a component as signal-based, you have to set the property of the signal in the component decorator to true. Given that most newly generated components in newer projects will be signal-based and standalone, the Angular team is working on a solution to mark components as both standalone and signal-based by default when generating new components to reduce the boilerplate.

Lifecycle methods in signal-based components

Traditional zone-based components have eight different lifecycle methods, but since they are tightly coupled with the current Angular change detection model, they are redundant when using signals.

In signal-based components, only ngOnInit and ngOnDestroy exist from the original lifecycle methods, while three new ones are proposed to be added to supplement signal-based components. These should include afterNextRender, afterRender, and afterRenderEffect.

Inputs and outputs in signal-based components

In signal-based components, all component inputs are proposed to be signals. This means you should be able to use them as regular read-only signals out of the box. Unlike traditional inputs, signal inputs are not decorators!

Angular Signals do not affect traditional outputs, but in order to be consistent with inputs API, a new output function will be introduced with a syntax similar to the inputs.

Signal-based components additionally have access to a new type of input, model inputs. This creates an input-output binding between parent and child components. The syntax proposed is banana in a box, similar to the ngModel.

Always bet on Angular

The introduction of Angular Signals is not just another Angular rewrite like the move from AngularJS to Angular 2. It is an additive change, a gradual change, and it’s completely backward-compatible.

Signals don’t mean that you can throw everything you know out the window immediately. They work on a full opt-in basis. You can refactor, use them only in the new features you develop, mix and match with RxJS or simply continue to use RxJS. All of your RxJS knowledge is still relevant. No approach is wrong here, and it is completely up to you or your team to decide which one to take.

That being said, the logical next step would probably be to try signals out and use them on newer features or features built from scratch and just take it from there.

Overall, signals are a huge change and a major leap forward for Angular.

Alongside new standalone APIs, signals make entering into the Angular ecosystem simpler with the added benefit of better performance.

Signals will substitute many RxJS patterns, which, although very logical once you get the hang of them, have a steep learning curve for beginners, which tends to put people off.

We believe Angular is a great choice for projects as it is very stable, performant, comes with almost all of the features you will need out-of-the-box and is now easier to get started with than ever before. As signals are currently in development preview, consider this blog post a mere Part 1.

Time, experience, and practical usage will reveal the best practices and patterns for signals. Always bet on Angular.