June 28, 2024
Angular Signals Part 3 – What are the advantages of Signal Inputs?
Klicke hier für die deutsche Version des Blog-Posts.
Increase the reactivity of your application with the Signal Inputs from Angular 17
In the first two articles in this series, Angular Signals Part 1 – How-to Guide on Angular Signals and Angular Signals Part 2 – How to combine Angular Signals and RxJS, we explained what Angular Signals are and how they can be combined with RxJS. Building on this knowledge, this time we want to take a closer look at the signal inputs introduced with Angular version 17 and find out what advantages they offer us in reactive programming. Signal Inputs are a reactive alternative to the traditional @Input()
decorators and are intended to simplify the exchange of data between child and parent components. This blog article uses an example to compare the classic @Input()
decorator with the new declarative approach of signal inputs and discusses the advantages of signal inputs.
If you want to learn more about Signal Inputs and Angular in detail, visit one of our Angular courses:
- Angular Basic Training: https://www.thecodecampus.de/schulungen/angular
- Advanced Training: https://www.thecodecampus.de/schulungen/angular-advanced
Classic @Input() decorator
Before we talk about signal inputs, let’s take a look at the @Input()
decorator. The @Input
decorator in Angular enables the transfer of data from a parent component to a child component. It is used in the child component to mark a property as input, the data is then provided by the parent component through property binding. This is a fundamental part of reusable components and scalable Angular applications, ensuring a clear separation of responsibilities and easy communication between components. More information on the @Input()
decorator can be found in the official Angular documentation. We would like to take a closer look at this using our already familiar example of even and odd numbers. The parent component passes an incrementable number to the child component as soon as it changes. The child component then checks whether the number is even or odd.
Parent-Component (main.ts)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... @Component({ ... template: ` <button (click)="increment()">Increment</button> <tcc-input-with-decorator [number]="currentNumber"></tcc-input-with-decorator> ` }) export class AppComponent{ currentNumber:number = 0; increment() { this.currentNumber++; } } ... |
The parent component AppComponent
has the member variable currentNumber
, which is incremented by the increment()
method. This method is triggered by clicking the button in the template. The variable is transferred to the child component InputWithDecoratorComponent
using property binding.
Child-Component (input-with-decorator.component.ts)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
... @Component({ selector: 'tcc-input-with-decorator', template: `<p>{{ number }} is {{ isEven ? 'Even' : 'Odd' }}</p>`, ... }) export class InputWithDecoratorComponent implements OnChanges { @Input() number: number | undefined; isEven: boolean | undefined; ngOnChanges(changes: SimpleChanges) { if (changes['number']) { this.isEven = changes['number'].currentValue % 2 === 0; console.log('Even? ',this.isEven); } } } |
The child component InputWithDecoratorComponent
receives a number from the parent component via the @Input()
decorator. The calculation of whether this number is an even or odd number is checked in the lifecycle method ngOnChanges()
. This method is called automatically when the input value of our @Input()
decorator changes. The template then displays whether it is an even or odd number.
Signal Inputs with input()
Signal Inputs are a reactive alternative to the traditional @Input()
decorators and are intended to simplify data exchange between child and parent components. They are suitable for both components and directives. The initialisation/declaration is very simple:
1 2 |
number = input(0); // InputSignal<number> name = input<string>(); // InputSignal<string|undefined> |
As with the variable number
, the input can be initialised with a value. Otherwise, as with the name
variable, undefined is automatically used. These signal inputs are optional inputs. The required inputs are dealt with in the Options section.
The InputSignal<T>
class extends the Signal<T>
class and can therefore be used within the signal context. This allows us to access the effect()
and computed()
methods.
1 |
export class InputSignal<T> extends Signal<T> { ... } |
Replace @Input() with Signal Input
Now let’s demonstrate the previous example using signal inputs. First, let’s look at the parent component again. Fortunately, only the selector of the component needs to be adapted. As before, the values can be transferred to the child component via property binding in the template.
Parent-Component (app.component.ts)
1 2 3 4 5 6 7 8 9 10 |
... @Component({ ... template: ` ... <tcc-signal-input [number]="currentNumber"></tcc-signal-input>`, }) export class AppComponent{ ... } |
computed() instead of ngOnChanges()
There are a few more changes to the child component:
The member variable number
has been converted to a signal input. Instead of reacting to changes through the ngOnChanges()
lifecycle method, we use the computed()
function. The computed()
function is not exclusive to signal inputs but, as mentioned before, can be used for any type of signal. In our previous articles, we described that this is a readonly signal that derives its value from other signals. The function is executed as soon as a signal within the callback function changes. In our case, when number
is incremented by the parent component.
It is important to note that the value of a signal (input) is always accessed with a function call ()
, otherwise an error is thrown.
Child-Component (signal-input.component.ts)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... @Component({ ... selector: 'tcc-signal-input', template: `<p>{{ number() }} is {{ isEven() ? 'Even' : 'Odd' }}</p>`, }) export class SignalInputComponent { number = input<number>(0); isEven = computed(() => this.number() % 2 === 0); sideEffect = effect(() => { console.log('Even? ', this.isEven()); }); } |
The effect()
function allows us to specify which actions should be executed when the value of a signal changes or which side effects should occur. In our example, we use a simple console.log()
statement that is executed when the isEven()
signal changes. This function can be particularly useful in structure directives, for example, to adjust or extend the behavior of DOM elements in Angular based on conditions or events. This allows DOM elements to be added or removed as required.
Options for Signal Inputs
Signal Inputs have the same options as @Input()
. We will now look at how you can use these options with signal inputs.
required
To mark a signal input as required, we can use required:
1 |
number = input.required<number>(); |
Without this option, the input is optional and does not have to be passed by the parent component. If the signal input is required, we receive an error if we do not enter a value in the parent component.
alias
We can also define an alias for our signal input:
1 |
number = input<number>({alias:"counter"); |
Now we can transfer our data using property binding with the counter
property:
1 |
<tcc-signal-input [counter]="currentNumber"></tcc-signal-input>`, |
transform()
The transform() function can be used to transform the value of the input property before it is output via the Signal Input. In the following example, the input value is multiplied by 2 before it is output via the Signal Input:
1 2 3 4 5 |
number = input(0, { transform: (value: number): number => { return value * 2; } }); |
So what are the advantages of signal inputs?
Signal inputs offer a direct and reactive alternative to @Input()
and therefore numerous advantages. The main advantage is especially noticeable when components have already been developed in a reactive style with Signals. With Signal Inputs, we can now also use inputs that are Signals. This allows us to take full advantage of the functionality of Signals by using computed()
and effect()
to react to value changes instead of using ngOnChanges
or setter, for example. Another advantage of Signal Inputs, which we did not explicitly address in the above example, is that they automatically mark OnPush components as dirty, which allows for more efficient UI updates. In addition, Signal Inputs offer the same options as @Input()
, including the ability to mark inputs as required
, use alias
and apply transform
. The usual usage options are therefore retained.
Experience is the best teacher: Hands-on with Signal Inputs
Are you ready to put theory into practice? In this interactive StackBlitz example, you will find the presented sample application, which takes up the basic concepts of Signal Inputs and includes all the examples and functions described. Dive in and experiment to experience Signals and Signal Inputs in action!
What happens next
Our journey with Angular Signals is not over yet. In the next article in this series, we want to introduce you to ngxtension and two extensions that have helped us a lot in the migration from @Input
to Signal Inputs and @Output
and with which we were able to migrate to the new signal-based paradigm in an efficient and structured way.