What’s the untracked function? [Angular Signals]

Alain Chautard
Angular Training
Published in
3 min readApr 18, 2024

--

In a past post, I discussed the importance of signal-based components and how such components will change the way we architect Angular applications for better performance.

I’ve also mentioned the computed() function used to derive values from any number of signals. For instance:

name = signal("Al");
lastName = signal("Test");

fullName = computed(() => this.name() + " " + this.lastName());

In the above example, changing the value of either name or lastName will automatically update the value of fullName.

What happens with computed() is that any signal read inside the computed function becomes a dependency of the new computed signal.

Angular Certification Exam

On paper, this sounds great, but what if we need to compute a value when a signal A changes and just read the current value of another signal B when A changes? This means that updating A would trigger the computation, not updating B.

In other words, what if we wanted fullName to update only when name changes, but not when lastName changes?

The solution to that problem is a function called untracked:

import { computed, signal, untracked } from '@angular/core';

fullName = computed(() => this.name() + " " + untracked(this.lastName));

untracked allows reading the value of a signal without making such signal a dependency of our computed signal or effect.

Let’s consider the following example also accessible on Stackblitz:

name = signal('Angular');
counter = signal(0);

info = computed(
() =>
`The name is now "${this.name()}" and the
counter value was ${untracked(this.counter)} when
the name changed.`
);

In this example, increasing the counter doesn’t trigger a new computation of info. Only a new name does so:

Note that sometimes, we might read another signal without being aware because such signal is read by a service or another dependency. In that case, this other signal would also become a dependency of our computed signal or effect.

The solution in that scenario relies on using untracked again with a slightly different syntax, passing a function it it:

effect(() => {
const name = this.name();
untracked(() => {
// If the `counterService` reads other signals, they won't
// become dependencies of this effect.
// Only this.name() is a trigger of this effect
this.counterService.count(`User name changed to ${name}`);
});
});

Any code called in the function passed to untracked could be reading other signals, and such signals won’t become dependencies of the effect/computed function.

This can be very helpful, for instance, to avoid infinite loops of signal updates, and it should be a best practice to implement even if such code doesn’t read other signals right now.

For instance, in the above example, this.counterService.count() might not read other signals as-is, but another developer could add signals in that implementation two months from now and have no idea that they created a dependency on an effect in a different part of the application.

As a result, I’d recommend using untracked every time a computed function or effect calls external code unless we know that we want to depend on other signals read by such external code.

My name is Alain Chautard. I am a Google Developer Expert in Angular and a consultant and trainer at Angular Training, where I help web development teams learn and become comfortable with Angular.

If you enjoyed this article, please clap for it or share it. Your help is always appreciated. You can also subscribe to my articles and the Weekly Angular Newsletter for helpful Angular tips, updates, and tutorials.

--

--