Angular Signal Inputs are here to change the game 🎲
Angular has gone through a lot of changes in the past few years. Since the release of 2.0 Angular embraced dectorators and used them to annotate parts of code that should be processed by Angular. We have Component
, Directive
, Pipe
, Injectable
decorators for classes etc. We also have @Input
and @Output
decorators that are used to define inputs and outputs of a component (public API of a component).
In this article, we will see how Angular is going to reduce the decorator usage for Inputs by using signal inputs and how it will make the code more readable, easier to understand.
Decorator Inputs
Let's start with a simple example of a component that has an input property.
The @Input
decorator is used to define an input property. The user
property is an input property and it is used to pass a User
object to the component. The User
object is then used to render the name and email of the user.
The code above looks ok, but it has a few issues 🤔.
Just by looking at the type, we can see that the user
will always be set (it's not optional). But, we're still allowed to use the component as:
But, this will result in an error at runtime because the user
property is not set.
Error: Cannot read property 'name' of undefined
We can fix this by making the user
property optional:
Now, we need to check if the user
is set before using it:
or by using the ?
operator:
This is a lot of code for something that should be simple. We can fix this by using a default value:
Now, we can use the user
property without checking if it is set. But, what if the user
object has a lot more fields? We would need to set all of them to a default value. This is not a good solution 🙃.
What we can do is to make the user input required if it's a core part of the component. We can do this by using the { required: true }
option:
This will make the user
input required and we will get an error if we don't set it.
This will result in an error:
How can we make these errors appear earlier?
What we can do is to make enable strictPropertyInitialization
in the tsconfig.json
file. This will make the compiler check if all properties are initialized. The code below will result in an error:
In order to fix this TS error, we either need to set a default value or just set it to undefined
or null
:
And then, just like before we need to check if the user
is set before using it.
If we try to make the user
input required, we will get the same error:
We can fix this by using the !
operator:
This is fine, because we're telling the compiler that the user
property will be set before we use it.
Signal Inputs
In Angular v17.1, a new feature was introduced - Signal Inputs. This is how they will look like:
The code above is equivalent to the code below:
The difference is that by default if we don't make the input required, the user
property will be set to undefined
. This is the first thing that signal inputs protect us from.
Required Inputs
If we want to make the user
input required, we can do it by using the required
option:
This is equivalent to:
What we can also do is to set a default value:
Because the user will always be instantiated, we won't need to use the ! (non null assertion) anymore 🚀
As we can see, we don't have to fight with TS anymore, or have to understand all the inner workings of Angular. We can just use the input
function and we're good to go 🚀.
Why doesn't Angular make all decorator inputs required by default?
The reason is that Angular is used in a lot of different projects. Making all inputs required by default will break a lot of projects. That's why we need to opt-in for this feature.
Deriving state from inputs
Input setter with method call
With class fields, what we did to derive state was to use input setters and call a method that will derive the state from the inputs.
Input setter with BehaviorSubject
Input setter have the issue of not having access to all input changes, that's why we used it mostly with BehaviorSubjects, and handled the racing conditions with rxjs operators.
ngOnChanges
ngOnChanges on the other hand, is called for every input change and it has access to all the inputs.
These are some of the patterns that we used to derive state from inputs.
Signals 🚦
Then signals were introduced and we started to use them to derive state from inputs.
Basically, we just refactored the code to use signals instead of BehaviorSubjects. And computed instead of combineLatest.
Signal Inputs
Now, with signal inputs, we can just use the input
function and we're good to go 🚀.
Inputs + API calls
This was easy to do with BehaviorSubjects, because we can just pipe the observables and handle the racing conditions with rxjs operators.
With ngOnChanges
we can do the same thing, but we need to handle the racing conditions ourselves.
That's too much code for something that should be simple. And also, if the api call depends on more than one input, we need to handle the racing conditions ourselves.
Signal Inputs + API calls
In order to listen to input changes, we use the effect
function.
In v17, effect
is scheduled to run after all the inputs are initialized. So, it means even if we have effect in the constructor, we can be sure that all the inputs are initialized (inside of it).
This code will result in the following output:
Because of this, we can use the effect
function to make API calls.
This is great, until we have to manage multiple input changes, and derive state from them, while also maintaining the subscriptions.
computedFrom (ngxtension) to the rescue
computedFrom
was born out of the need to derive state from multiple sources (observables, signals, signals from inputs) and also to manage the subscriptions in a clean way (without having to unsubscribe manually).
Read the whole story here:
A sweet spot between signals and observables 🍬
This is how we can use it to derive state from inputs and api calls:
If you have more sources, you can just add them to the array:
Migrating to Signal Inputs
You can go over all your inputs and start refactoring every input and its references one by one, or you can use some migration schematics that handle most of the basic usage patterns for you.
The ngxtension library publishes some schematics you can use in an Angular or Nx workspace.
To use it you just have to run these two commands:
Find more about it in the docs: Ngxtension Signal Inputs Migration by Chau Tran 🪄
To sum up
With signal inputs, we can just use the input
function and we're good to go 🚀. We don't have to fight with Typescript
anymore or have to understand all the inner workings of Angular
. We can just use the input
function to derive state directly from inputs, to manage API calls more easily, and if we don't want to maintain any glue code at all, computedFrom
(ngxtension) is there to help us.
The article was reviewed by:
- Matthieu Riegler 🧑🏻💻 (Make sure to follow him on Twitter for everything Angular)
Thanks for reading!
If this article was interesting and useful to you, and you want to learn more about Angular, support me by buying me a coffee ☕️ or follow me on X (formerly Twitter) @Enea_Jahollari where I tweet and blog a lot about Angular
latest news, signals, videos, podcasts, updates, RFCs, pull requests and so much more. 💎