5 mistakes developers make when using Signals in Angular
Signals have been with us for a while now. While development practices are still emerging, we can spot some misusages or oversights already. Addressing those will help you make your application better prepared for the future signal components and improve performance, as well as avoid/fix some issues with the code logic.
Mistake #1. Not using signals for a dynamic state
When developing a new/modern application, it’s important to use signals properly from the start. The proper usage of signals would be using them for the state that can change during the application lifecycle and that gets rendered in templates.
E.g., even on my current project where we are using the latest features (even those that are in the preview) some developers tend to not use signals for the state. Instead, they use them the old way.
@Component({})
export class MyComponent {
stateThatChanges = '';
changeState(newState: string) {
this.stateThatChanges = newState;
}
}While this code will work, it’s not good to put such “surprises” in a modern codebase that heavily relies on signals. Such places, in the future with the incorporation of the signal components, will backfire because the state change would not be reflected.
It’s both easy and better to use signals instead. You are not losing anything, only gaining. You can even update a signal from a subscription, and you won’t need to call the markForCheck() ! Angular will see that the signal is used in the template, and it will run the change detection (a more optimized one!) for that component for you!
Mistake #2. Using signals when they are not needed
This is not a big issue compared to the previous one. However, IMO it’s better not to use signals for things such as flags that are used only in the code’s logic. I.e., when the value is not being rendered.
For example, let’s take the following scenario:
@Component()
export class MyComponent {
firstClick = signal(true);
handleClick() {
if (this.firstClick()) {
this.firstClick.set(false);
return;
}
// Do something on the following clicks
}
}In this scenario, we only use the signal to perform some checks and continue code execution based on it. The signal is not rendered in the template. Therefore, it’s better to keep it as a plain property. To improve it even more you would want to make it private by adding # .
Mistake #3. Unnecessary conversions
This one is also a small, and more of a verbosity issues. Usually, it occurs when developers have an observable (e.g., valueChanges ) and, for some reason, they convert it to a signal only to create an effect to handle that signal.
@Component({})
export class MyComponent {
form = new FormGroup({...})
constructor() {
const formValue = toSignal(this.form.valueChanges)
effect(() => {
const value = this.formValue()
// handle value
})
}
}While this works, the conversion, to me, looks rather redundant and adds more code with no value. Furthermore, effects work very differently from observables. They are relying on the change detection, whereas observables run immediately upon emission.
So, I’d rather use the following approach. It does require you to handle the unsubscription yourself, but reduces unnecessary conversions as well as gives a more predictable timing.
@Component({})
export class MyComponent {
form = new FormGroup({...})
constructor() {
this.form.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => {
const value = this.formValue()
// handle value
})
}
}Mistake #4. Not analyzing what you write in the effects
While effects are easy to use, they are also easy to misuse. Specifically, because they subscribe to all signals that were read during their execution. This is especially dangerous when you are calling some methods of other services/components in the effect because they may have other signal reads. This would create unexpected subscriptions, and hence, effect runs.
Let’s imagine the following scenario. You have a multi-account system where a user can switch the current account using the UI. When they log in, they have a default account that is used before they decide to select a specific account manually. And, you have to make some requests on each account change to get the account-related data.
@Component()
export class AppComponent {
#userStore = inject(UserStore);
constructor() {
effect(() => {
const currentAccount = this.#userStore.currentAccount();
const defaultAccount = this.#userStore.defaultAccount();
const activeAccount = currentAccount() || defaultAccount();
if (!activeAccount()) return;
// fetch data
})
}
}Now, let’s imagine that the user logs in and, by some reason, the defaultAccount is set to 1. Later currentAccount is also set to 1. Now, the question — how many times will the request be executed? Two. Two times will it execute. Why? Because, at first, the first signal changes, and, then, the second one. While it may end up having the UI look okay, it’s suboptimal to do several identical requests.
To fix this we can create a computed that would return only value. And it would trigger the effect only when the value is different. I.e., it will simply skip the second emission since the primitive values from both signals are equal now.
@Component()
export class AppComponent {
#userStore = inject(UserStore);
constructor() {
const activeAccount = computed(() => this.#userStore.currentAccount() || this.#userStore.defaultAccount())
effect(() => {
if (!activeAccount()) return;
// fetch data
})
}
}Now, the effect has only one signal to track. Neat!
However, to address possible signal reads in other methods etc, it’s better to write signals with that in mind upfront. Before you encounter such issues. Here you could be using untracked() from the Angular framework to execute the body of the effect (while reading signals needed outside untracked), or use a nice utility called explicitEffect from the ngxtension library.
The code above would transform into the following
@Component()
export class AppComponent {
#userStore = inject(UserStore);
constructor() {
const activeAccount = computed(() => this.#userStore.currentAccount() || this.#userStore.defaultAccount())
explicitEffect([activeAccount], ([account]) => {
if (!account) return;
// fetch data
})
}
}Now, it becomes very similar to the React’s effects. Only the dependencies specified in the array are tracked and you can safely call any methods in the callback.
Mistake #5. Using async pipe instead of signals
While this one is not strictly a mistake, it gives you a slightly worse DX compared to signals. E.g., if you were to use everywhere in your app only signals — you wouldn’t even include the async pipe in your bundle! On top of that, you’d have an easy access to the current state in a synchronous way.
@Component({
template: `
@for (let item of items$ | async; track item.id) {
<span>{{ item.name }}</span>
}
<!-- Instead write -->
@for (let item of items(); track item.id) {
<span>{{ item.name }}</span>
}
`
})
export class MyComponent {
#service = inject(Service)
// If I needed to read the items$ value in the TS code, I'd need to
// use `shareReplay` and subscribe.
// Also, it forces me to use and import the async pipe
items$ = this.#service.getData()
// Here, the we can always just call `signalItems()` in the TS code and
// get the current value without any waiting!
signalItems = toSignal(this.#service.getData())
}This does not mean, however, that you must convert all your observables to signals. In general, you should prefer signals for rendering data. Observables would still be used for network requests, e.g., form submissions.
