Angular continues its push toward full application-wide reactivity, and a significant milestone in that journey is the introduction of Signal Inputs. This feature, which first appeared in Angular 17.1, offers a modern alternative to the well-established @Input decorator.
Here, we will examine all the configuration options available for input signals and explore how they can serve as a more effective replacement for the traditional OnChanges lifecycle hook.
Let's dive into the world of Angular signal inputs.
Table Of Contents
- Transitioning from @Input to a Signal Input
- The Core Advantage of Signal Inputs
- Impact on the Parent Component
- Substituting
OnChangeswith a signaleffect() - Signal input configuration: required, alias, and transform
- Implementing alias properties for signal inputs
- Creating input transforms for signal inputs
- Summary
For a video walkthrough of signal inputs, you can check out this resource on the Angular University YouTube channel:
Transitioning from @Input to a Signal Input
The most straightforward way to grasp signal inputs is by contrasting them with the conventional @Input decorator.
You are likely well-versed in the Angular @Input() decorator, a fundamental building block of the framework.
If not, a detailed breakdown is available here: Angular @Input: Complete Guide.
Let's take a basic component built with the @Input decorator and see how it translates to one using signal inputs.
We'll start with a simple CounterComponent used in our application:
@Component({
selector: "app-root",
imports: [CounterComponent],
standalone: true,
template: `<counter [value]="counter" />
<button (click)="onIncrement()">
Increment
</button>`,
})
export class AppComponent {
counter = 10;
onIncrement() {
this.counter++;
}
}
This component's sole purpose is to receive a counter value and render it.
Let's examine the CounterComponent:
@Component({
selector: "counter",
standalone: true,
template: ` <h1>Counter value: {{ value }}</h1>`,
})
export class CounterComponent implements OnChanges {
@Input()
value = 0;
ngOnChanges(changes: SimpleChanges) {
const change = changes["value"];
if (change) {
console.log(
`New value: ${change.currentValue}`);
}
}
}
As shown, it's a simple component that logs new counter values to the console.
The CounterComponent is built using the traditional @Input decorator.
Notice the ngOnChanges lifecycle hook, which is in place to detect when a new value is assigned to the @Input value property.
Whenever there's a change to an input property—specifically, the counter value—we fetch the current value and log it.
This is all standard Angular; nothing out of the ordinary.
Now, let's reimagine this exact example, but this time, we'll use input signals!
Here's what our CounterComponent will look like after the conversion:
@Component({
selector: "counter",
standalone: true,
template: ` <h1>Counter value: {{ value() }}</h1>`,
})
export class CounterComponent {
value = input(0);
}
And that's it! Our code now functions in the same manner.
Or almost...
Pay attention to the use of () when accessing the value in the template:
<h1>Counter value: {{ value() }}</h1>
`,
You might question why the () is required and why we can't simply access the value directly like so:
<h1>Counter value: {{ value }}</h1>
`,
The problem is, if you attempt to run this, it won't work!
You'll encounter a strange error, with a function being displayed in place of the actual value:
The counter value is function inputValueFn() { producerAccessed(node); if (node.value === REQUIRED_UNSET_VALUE) { throw new RuntimeError(-950, ngDevMode && "Input is required but no value is available yet."); } return node.value; }
And here's an excerpt of the stack trace:
Error in src/main.ts (8:42)
value is a function and should be invoked: value()
So, what's happening here?
The value property is no longer a plain numeric value.
Instead, it's a signal input property, which emits numeric values.
Consequently, value is no longer a number; it's a Signal<number>.
Similar to any signal, you must call it using () to retrieve its current value.
For more on signals, refer to this comprehensive guide: Angular Signals: Complete Guide.
The Core Advantage of Signal Inputs
The primary benefit becomes apparent when you're building components in a reactive, signal-based style.
With signal inputs, all component inputs are themselves signals, making it trivial to create computed signals from them, define effects that respond to input changes, and so forth.
This facilitates a much smoother adoption of a reactive, signal-driven approach to component authoring.
Impact on the Parent Component
It's worth noting that after refactoring to use input signals, no changes are required in the parent component for the CounterComponent to operate correctly.
In the parent component, we still have:
@Component({
selector: "app-root",
standalone: true,
template: `<counter [value]="counter" />
<button (click)="onIncrement()">
Increment
</button>`,
})
export class AppComponent {
counter = 0;
...
}
We continue to pass the counter value to the CounterComponent through the value property, exactly as we would when using the @Input decorator.
Substituting OnChanges with a signal effect()
What other benefits do signal inputs offer?
Recall that in our previous code with the @Input decorator, we could monitor changes in the @Input using the ngOnChanges lifecycle hook.
This is still possible with signal inputs, but this time, we can leverage the effect API:
@Component({
selector: "counter",
standalone: true,
template: `<h1>Counter value: {{ value() }}</h1>`,
})
export class CounterComponent {
value = input(0);
constructor() {
effect(() => {
console.log(`New value: ${this.value()}`);
});
}
}
It's important to note that the effect API is not exclusive to signal inputs; it's a general-purpose API that works with any signal.
You can find more information about it in this signals guide.
Let's break down what we just did with the effect API:
- We set up a constructor and created an effect.
- Inside the effect's callback, we logged the current value of the signal input (the new counter value) to the console.
- We accessed the new counter value from the signal input
valueusing thethiskeyword. - Reading the signal input
valuewiththisinside the effect declares it as a dependency, causing theeffectto re-run whenever the signal input changes.
If you test this, you'll see that this new version works flawlessly, just like the initial one based on OnChanges.
You'll also notice that the new code using a signal input and an effect is considerably more readable.
This is one of the key advantages of input signals: for most cases, we no longer need to rely on OnChanges.
Let's now explore the other options available for signal inputs.
Signal input configuration: required, alias, and transform
Signal inputs come with the same options and defaults as a standard @Input.
For instance, like with @Input, a signal input is optional by default.
However, if you need to make it required, you can do so like this:
@Component({
selector: "counter",
standalone: true,
template: `<h1>The counter value is {{ value() }}</h1>`,
})
export class CounterComponent {
value = input.required<number>();
...
}
Now, this becomes a required input!
Note that even though this property is required, we don't need to provide an initial value.
This is because the initial value is expected to come from the parent component via the template.
If you attempt to run this component without an input value, an error will be thrown.
[ERROR] NG8008: Required input 'value' from component
CounterComponent must be specified. [plugin angular-compiler]
Implementing alias properties for signal inputs
Similar to @Input, you can define an alias for your input property:
@Component({
selector: "counter",
standalone: true,
template: `<h1>The counter value is {{ value() }}</h1>`,
})
export class CounterComponent {
value = input(10, {
alias: "counter",
});
...
}
You can then pass the value to the CounterComponent using the counter property, instead of value:
<counter [counter]="counter" />
Creating input transforms for signal inputs
It's also feasible to define a transform function for the signal input property.
The transform option allows you to modify the property's value before it's emitted by the signal input.
Suppose we want to transform the incoming value and multiply it by 100:
@Component({
selector: "counter",
standalone: true,
template: ` <h1>counter value: {{ value() }}</h1>`,
})
export class CounterComponent {
value = input(10, {
alias: "counter",
transform: (value: number) => value * 100,
});
}
When we click the increment button in the parent component, the transformation will be applied as anticipated:
1000
1100
1200
And with that, we've covered all the available options for signal inputs!
As you can see, signal inputs function just like a standard @Input, with the key difference being that they are reactive and signal-based.
Thank you for reading. If you'd like to stay updated on similar posts, feel free to subscribe to the newsletter:
You'll also receive the latest news about the Angular ecosystem.
And for an in-depth look at all the features of Angular Core, including Signals, consider the Angular Core Deep Dive Course:
Summary
In conclusion, input signals represent a substantial leap forward on the path to comprehensive Angular reactivity.
In essence, input signals behave like a regular @Input, except that they are signal-based.
They support all the standard options as @Input, including required, alias, and
transform.
We also demonstrated how the effect API can serve as a replacement for the ngOnChanges lifecycle hook when building components with signals.
If you have any questions or feedback, please don't hesitate to reach out!
