Original cover photo by Shyam on Unsplash.
If we ask any Angular developer "What is the hottest topic in Angular right now?", with almost 100% certainty everyone would respond "Signals!". And they would be right. Signals are the hottest topic in Angular right now.
Of course, there is now a multitude of articles, videos, blog posts and so on, describing in deep detail what signals are, how they work, and different use cases, so we can safely assume almost everyone in the Angular community now knows about signals, and maybe at least has tried them out. But a fact that became quite obvious recently is that the community needs a specific set of rules for working with them, a set of Do-s and Don't-s, if you will. In today's article, let's dive into the world of signals, and find out examples of use cases where using signals in a specific way is not a good idea, and see what we should do instead. Let's get started!
Don't use setters for inputs to turn them into signals
When signals first dropped in v16, support for lots of features was limited (as signals themselves were experimental). For instance, it was impossible to use signals as inputs to components. So, the community came up with a workaround: using setters to turn inputs into signals. The idea is simple: we create a setter for an input and a separate signal, and inside the setter we set that signal's value to be the value of the input. Then, we can use the signal as an input to the component.
@Component({
selector: 'app-my-component',
template: `
<div>
{{ inputSignal() }}
</div>
`
})
export class SomeComponent {
inputSignal = signal<string>();
@Input() set input(value: string) {
this.inputSignal.set(value);
}
}
Obviously, this is quite verbose, especially if we have lots of inputs. This is really a lot of boilerplate, so let's see what we can do about it.
Do use signal inputs
Starting from Angular v17.1, a new way of declaring input properties for components and directives has emerged: the input
function. This function declares an input, but its value is a signal, rather than a plain property. We can simplify our example now:
@Component({
selector: 'app-my-component',
template: `
<div>
{{ inputSignal() }}
</div>
`
})
export class SomeComponent {
inputSignal = input<string>('default value');
}
Note: input signals are not stable as of v17.1, but will move to stable soon.
Of course, this is much, much better. This also supports all the features we have with regular inputs, for instance, to create a required
input, we can do this:
export class SomeComponent {
inputSignal = input.required<string>(); // we do not need to provide a default value here
}
We can also use transformers:
export class SomeComponent {
booleanSignal = input(true, {transform: booleanAttribute});
}
And aliases:
export class SomeComponent {
inputSignal = input.required({alias: 'condition'});
}
Warning: signal inputs are read-only, it is not possible to set another value to it in the child component
Of course, the previous approach was a workaround for a missing feature, and with time all Angular developers will move to signal inputs. Now, let's focus on some more serious stuff, that can happen in any codebase.
Don't retrieve data via HTTP calls in effects
This is a common mistake that can happen in any codebase. Let's say we have an input in our template, and we want to make an HTTP call when the input changes. We can do this:
@Component({
selector: 'app-my-component',
template: `
<input (input)="query.set($event.target.value)" />
<ul>
@for (item of items) {
<li>{{ item.name }}</li>
}
</ul>
`
})
export class SomeComponent {
query = signal<string>();
items: Item[] = [];
constructor(private http: HttpClient) {
effect(() => {
this.http.get<Item[]>(`/api/items?q=${this.query()}`).subscribe(items => {
this.items = items;
});
});
}
}
However, this poses several problems. First of all, the items
collection is not a signal (we can make it into a signal, but then we would have to set {allowSignalWrites: true}
on the effect, which is an even worse practice), which will make it hard to work with zoneless change detection in the future. Next, the declaration of items
is separate from the place where its value is actually set, making it a bit harder to understand how it works; and finally, the triggering of the effect is decoupled from RxJS, meaning we cannot really harness the power of timing operators (like debounceTime
in order to prevent unnecessary HTTP calls), as each change to the query
signal will create a separate Observable
.
Do use toSignal
+ toObservable
However, there is an easy way to circumvent all of those problems by utilizing the interoperability between signals and RxJS. Let's make a cleaner and more powerful way of doing this:
@Component({
selector: 'app-my-component',
template: `
<input (input)="query.set($event.target.value)" />
<ul>
@for (item of items()) {
<li>{{ item.name }}</li>
}
</ul>
`
})
export class SomeComponent {
http = inject(HttpClient);
query = signal<string>();
items = toSignal(
toObservable(this.query).pipe(
debounceTime(500),
switchMap(query => this.http.get<Item[]>(`/api/items?q=${query}`))
),
);
}
Here, we first move to the RxJS land with toObservable
, make our asynchronous stuff there, and then move back to signals with toSignal
. This way, we can use all the power of RxJS, and still have a signal in the end, and additionally, we solve the timing issue of multiple requests being made when the user types fast.
Let's move towards a more obscure example.
Don't forget about untracked
Let's imagine we have a CompanyDetailsComponent
page that displays company data, and a list of its employees, and allows general editing, for instance, we can change the description text of the company. There can be a lot of employees, so we want to be able to search through them with a query, as in the previous example. However, we are not searching through the entirety of employees, but only through the ones that are employed at this particular company, meaning that every time we search for an employee, we have to use the company ID too. We can do this:
@Component({
selector: 'app-company-details',
template: `
<div>
<h2>{{ company().title }}</h2>
<input placeholder="Company description"
[ngModel]="company().description"
(ngModelChange)="updateCompanyDescription($event)" />
</div>
<input (input)="query.set($event.target.value)" />
<ul>
@for (employee of companyEmployees()) {
<li>{{ item.name }}</li>
}
</ul>
`
})
export class CompanyDetailsComponent {
http = inject(HttpClient);
query = signal<string>();
company = input.required<Company>();
employees = input.required<Employee>();
companyEmployees = computed(() => {
return this.employees().filter(employee => employee.companyId === this.company().id && employee.name.includes(this.query()));
});
@Output() companyUpdated = new EventEmitter<Company>();
updateCompanyDescription(description: string) {
this.companyUpdated.emit({
...this.company(),
description
});
}
}
We can see that this example is somewhat complex. Let's break it down to simplify:
- The component receives the list of all employees and the company details
- We use a computed signal to get the list of employees working at this particular company and match the query
- We can update the company description, and emit an event when it happens so that the parent makes necessary HTTP requests
- We use the computed signal in the template
This will work just fine, however, we overlooked something: when the description gets updated, it is sent to the parent component, which will then send a new reference to the company
signal input, and because it is also used in the computed companyEmployees
signal, this will trigger the computation unnecessarily, as we know the id
cannot change, and we don't really care about the description
, or any other property for that matter.
This can become even worse if we use an effect to trigger an HTTP call to make the search through employees, resulting in a lot of unnecessary HTTP calls every time the user types something in the unrelated description
input.
Do use untracked
Thankfully, this issue has a very simple solution: using the untracked
function to read the signal's value instead of calling it the usual way. untracked
will return the current value of the signal without marking it as a dependency of the current computed signal/effect, meaning that the computation will not be triggered when the value of that signal changes. Here we go:
companyEmployees = computed(() => {
return this.employees().filter(employee => employee.companyId === untracked(this.company).id && employee.name.includes(this.query()));
});
Note: in general, be careful with signals being called in the
computed
oreffect
function callbacks, rigorously check if they really need to be a dependency, and useuntracked
if they don't.
Problem solved! Now, let's move to a really complex example, one that most people will overlook unless they are explicitly aware of this issue.
Don't use toSignal
in services to expose Observables as signals.
Lots of Angular applications do not use dedicated state management libraries like NgRx or Akita, and instead use services to manage the state of the application. This is a perfectly valid approach and can work really well. Most of such services use RxJS Observable
-s (more specifically Subject
-s or BehaviorSubject
-s) to propagate changes to the state throughout the application. Of course, now, it might be tempting to use toSignal
to expose the Observable
as a signal, so that the rest of the application can consume it directly. However, this can lead to a bunch of problems. One glaring one is that the signal will not be automatically disposed of when the component which uses it is destroyed, and the Observable
will keep emitting values, which might not be the desired outcome. Also, Observable
-s in general are way more versatile, and some components might choose to handle such values in a slightly different way, which will not be possible if the value is exposed as a signal. Let's view an example:
@Injectable({
providedIn: 'root'
})
export class MessagesStore {
private messagesService = inject(MessagesService);
private messages$ = this.messagesService.getMessages();
messages = toSignal(this.messages$);
unreadMessages = computed(() => {
return this.messages().filter(message => !message.read);
});
addMessage(message: Message) {
this.messagesService.addMessage(message);
}
}
In this scenario, we are reading messages in a live stream from some external service (FireBase, WebSocket, does not matter), and the source will connect to the service when the first subscriber appears, and disconnect when the last subscriber is gone. This is a perfect use case for an Observable
, but not really for a signal. While we got some benefits like using computed
, we also made sure that the connection starts as soon as the service is created, and does not really ever stop, even when the components that consume those signals are destroyed, as it is the MessagesStore
that is the subscriber, and it will not be destroyed until the application is closed.
Do use toSignal
in components or expose state via connecting methods
So, what can we do about this problem? Well, one straightforward way of achieving this can be just not exposing signals from services at all and only call toSignal
in components that consume the state. This way, we can be sure that the connection to the source will be established only when the component is created, and will be destroyed when all the consumer components are destroyed. This is a normal approach for most apps, however, it can be a bit tedious, as we might end up copy-pasting computed signals here and there (for instance, unreadMessages
in the previous example).
So, instead we can opt for using specific methods that "connect" a component to the Observable
, while still returning a signal that lives in the component's injection context, and not the service's. Let's see how we can modify the previous example to use this approach:
@Injectable({
providedIn: 'root'
})
export class MessagesStore {
private messagesService = inject(MessagesService);
private messages$ = this.messagesService.getMessages();
addMessage(message: Message) {
this.messagesService.addMessage(message);
}
messages() {
return toSignal(this.messages$);
}
unreadMessages(messages: Signal<Message[]>) {
return computed(() => {
return messages().filter(message => !message.read);
});
}
}
This way, we use the messages
method to actually get the overall messages, and in the case of other computed properties, we can use other methods that return computed properties. In the component, we can simply do this:
@Component({
selector: 'app-messages',
template: `
<ul>
@for (message of unreadMessages(messages())) {
<li>{{ message.text }}</li>
}
</ul>
`
})
export class MessagesComponent {
private readonly messages = inject(MessagesStore);
messages = this.messagesStore.messages();
unreadMessages = this.messagesStore.unreadMessages(this.messages);
}
This way, we can ensure the stream we subscribe to in the component is safely disposed of when the component is gone. Of course, there are other ways of achieving this, but we list the simplest ones here. You can try and find other ways that best suit your project and codebase, but remember not to use the toSignal
function in a shared service.
Conclusion
This article is meant to be a small ruleset for Angular developers when they deal with signals. Signals truly are exciting, and lots of teams now increasingly adopt them in their projects, but they can have caveats, and it is important to be careful about them. If you know more such issues, feel free to comment - let's see how we can solve them together!
Small promotion
As you might already know, I was very busy writing a book last year. It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v17), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it here. The manuscript is already entirely finished, with some polishing left to do, so it is currently in Early Access, with the first 5 chapters already published, and more to drop soon. If you want to keep yourself updated on the progress, you can follow me on Twitter or LinkedIn, where I will be posting whenever there are new chapters or promotions available.
Top comments (13)
Very informative, I have been guilty of using "allowSignalWrites" in effects, at least I updated signals :) .. but I love the patterns you showed here.
As a side note, I just ported one of our internal applications to use signal inputs(), and it's so awesome. The best thing? ngOnChanges are now no more needed, effect() and computed() to the rescue. Awesome.
NgOnChanges is really horrible to work with
Yes, signal inputs are clearly a superior approach! Totally agree. Thanks for appreciating the article :)
Thanks for sharing!
Informative Post well done keep it up
Regard
Danish Hafeez | QA Assistant
ictinnovations.com
Thanks for your appreciation!
Good article. Thanks.
Have recently upgraded to the latest Angular. Very excited to start working with Signal inputs!
Great to hear I have been of help to you!
Thank you for sharing.
Thank you for this amazing article, @armandotrue 👏
The toObservable-toSignal composition is elegant and it's the first time I've seen it.
However, doesn't this make the HTTP request side effect eager unless we use RxJS operators to guard against this? One thing you forgot is initializing the
query
signal with empty string (''
) or another bottom value. The thing about signals is that they, likeBehaviorSubject
s alway must have a current value, causing theitems
signal and its built-in effect to immediately (well, after 500ms) kick off.Do you have tips on the
allowSignalWrites
effect option, @armandotrue?Hi Armen Vardanyan,
Excellent content, very useful.
Thanks for sharing.
Good article. Thanks))
Thank you for sharing, Armen! Really useful.
Question about companyEmployees. How it will get updated if a company value is updated with selector for example and it is untracked?