In this post, we are going to cover the latest step in the Angular journey to full application-wide reactivity: Angular Signal Inputs.

We are going to compare this feature originally introduced in Angular 17.1 to its traditional counterpart, the @Input decorator.

We're going to cover all the available options for input signals and we're going to learn how signal inputs can provide us with a better alternative to the traditional OnChanges lifecycle hook.

So without further ado, let's get started learning Angular signal inputs!

Table Of Contents

  • Converting @Input to a Signal Input
  • So what is the advantage of using Signal Inputs?
  • What about the parent component?
  • Replacing OnChanges with a signal effect()
  • Signal input options: required, alias and transform options
  • Setting alias properties for signal inputs
  • Defining input transforms for signal inputs
  • Summary

If you would like to learn about signal inputs in video format, check out this video on the Angular University YouTube channel:

Converting @Input to a Signal Input

The best way to understand signal inputs is to compare them to the traditional @Input decorator.

You are probably already familiar with the Angular @Input() decorator, which is one of the cornerstones of the framework.

If not, you can check out here a detailed explanation about it: Angular @Input: Complete Guide.

Let's then take a simple component written using the @Input decorator and convert it to use signal inputs.

Let's start with a simple CounterComponent that we are using 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 is just a simple component that takes as an input a counter value and displays it on the screen.

Let's see 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 you can see, this is just a simple component that prints out to the console the new values of a counter.

The CounterComponent is written with the traditional @Input decorator.

See that we have the ngOnChanges lifecycle hook on the component, that we are using to detect when a new value is available for the @Input value property.

If there is a change in one of the input properties, namely the counter value property, we then retrieve the current value of the counter and log it in our console.

This is all traditional Angular, nothing new here.

Now, we are going to reimplement this same example but this time around using input signals!

Here is what our CounterComponent will look like after the refactoring:

@Component({
  selector: "counter",
  standalone: true,
  template: ` <h1>Counter value: {{ value() }}</h1>`,
})
export class CounterComponent {
  value = input(0);
}

And that's it! Our code now works in the same way.

Or almost...

Notice the use of () when accessing the value on the template:

<h1>Counter value: {{ value() }}</h1>
`,

You might be wondering why the () is necessary, and why we are not just accessing the value directly like this:

<h1>Counter value: {{ value }}</h1>
`,

The issue is, if we try to run this, it won't work!

We will get some weird error like a function printed out in place of the 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 is a partial stack trace:

Error in src/main.ts (8:42)
value is a function and should be invoked: value()

So what is going on here?

You see, the value property is no longer a plain numeric value.

Instead, it's a signal input property, whose emitted values are numeric.

So value is no longer a number, it's a Signal<number>.

So just like with any signal, we need to invoke it using () to get the current value.

If you want to learn more about signals, check out this other guide: Angular Signals: Complete Guide.

So what is the advantage of using Signal Inputs?

The main advantage is noticeable when you are writing your components in a reactive style, using signals.

Now all your component inputs are themselves signals, so it's very easy to derive computed signals from them, define effects that react to input changes, and so on.

So now with signal inputs it's much easier to write your components in a reactive, signal-based style.

What about the parent component?

Notice that after this refactoring to use input signals, we did not have to change anything in the parent component for the CounterComponent to work.

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 are still passing the counter value to the CounterComponent via the value property, as usual, just like in the case when we were using the @Input decorator.

Replacing OnChanges with a signal effect()

So what are some of the other benefits of signal inputs?

Remember, in our old code using the @Input decorator, we were able to get the changes in the @Input decorator using the ngOnChanges lifecycle hook.

We can still do that with signal inputs, but this time around using 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()}`);
    });
  }
}

Notice that the effect API is not specific to signal inputs, this is a general-purpose API that can be used with any signal.

You can read more about it on this signals guide.

Let's break down what we did above with the use of the effect API:

  • We created a constructor and created an effect
  • Inside the callback function in the effect API, we logged the current value of the signal input (which is the new counter value) to the console.
  • We accessed the new counter value from the signal input value using the this keyword.
  • Accessing the signal input value using the this keyword inside the effect API tells it that the signal input value is a dependency, so the effect will re-run whenever the signal input changes.

If you try this out, you will see that this new version is working perfectly, just like the initial version based on OnChanges.

You can also see that the new code with the signal input and effect is significantly easier to read.

So this is one of the several advantages of input signals: we no longer need to use OnChanges for most us cases.

Let's now look at some other options that we have available for signal inputs.

Signal input options: required, alias and transform options

Signal inputs have just the same usual options and defaults as a normal @Input.

For example, just like with @Input, a signal input is optional by default.

But if we instead want to make it required, we 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!

Notice that even though this property is required, we don't need to pass an initial value.

This is because the initial value should be passed by the parent component via the template.

If we try to run this component without an input value, we're going to get an error.

[ERROR] NG8008: Required input 'value' from component 
CounterComponent must be specified. [plugin angular-compiler]

Setting alias properties for signal inputs

Just like with @Input, we can define an alias name for our input property:

@Component({
  selector: "counter",
  standalone: true,
  template: `<h1>The counter value is {{ value() }}</h1>`,
})
export class CounterComponent {

  value = input(10, {
    alias: "counter",
  });
    ...
}

We can then pass the value to the CounterComponent via the counter property, instead of value:

<counter [counter]="counter" />

Defining input transforms for signal inputs

It's also possible to define a transform function for the signal input property.

The transform option allows us to transform the value of the property value before it gets emitted by the signal input.

Let's say that we want to transform the value passed in via the signal input property 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,
  });
}

If we click on the increment button in the parent component, the transformation will be applied as expected:

1000

1100

1200

And with this, we have covered all the available options for signal inputs!

As you can see, signal inputs work just like a standard @Input, except that they are reactive and signal-based.

I hope that you enjoyed this post, to get notified when new similar posts like this are out, I invite you to subscribe to my newsletter:

You will also get up-to-date news on the Angular ecosystem.

And if you want a deep dive into all the features of Angular Core like Signals and others, have a look at the Angular Core Deep Dive Course:

Summary

As you can see, input signals are a great step forward on the road to full application-wide Angular reactivity.

In a nutshell, input signals behave just like a plain regular @Input, except that they are signal-based.

They have all the same usual options as @Input, such as required, alias, and
transform.

We also saw how we can use the effect API to replace the ngOnChanges lifecycle hook if we are writing our components using signals.

If you have any questions or comments, let me know as well. I'll be happy to help!