Introduction
Virtually every Angular application centers around displaying information to an end user, and that user interacting with the application in some way. Angular has always had a way to react to changes in the data or state of the application, but it's been made more performant in recent years with the introduction of signals. Signals can be used in a lot of different situations in a component, but a great way to use them is in conjunction with inputs and outputs to components.
If you've used Angular before, you'll be familiar with the @Input and @Output decorators for components. Using signal inputs and outputs is not just a change in syntax, but a change in the overall reactivity of the application. In this article, we'll look at how to use these special types of signals and some of the benefits of using them.
Input Signals
The Basics
The @Input decorator is a frequently used feature of components and directives in Angular, allowing developers to pass data from parent components to children. However, traditional @Input properties rely on Angular's standard change detection to verify if values have changed. In a default setup, this means your app could potentially check every single component during each change detection cycle to ensure the UI matches the state. This performance bottleneck is where signals really shine. Because signals are reactive, Angular knows exactly when a value has updated. Instead of traversing the entire component tree, the framework can update only the specific parts of the DOM that changed.
Signal inputs for components give you this performance benefit with an intuitive API and easy use case. There are, however, some differences between the two variants of inputs. Let's explore signal inputs, and we'll look at those similarities and differences.
export class MyComponent {
@Input() name !: string;
firstName = input<string>();
}
The example above shows both ways of declaring inputs to the component. One of the big differences you'll notice is with the decorator, where we need to use the ! operator in the type declaration for the name variable. This is called the definite assignment assertion operator, and it notifies TypeScript that while there is no value currently assigned to the variable, you will ensure that a value is provided before attempting to use it. This operator is necessary for all inputs declared with the @Input decorator where you don't provide a default value for the input. The input declared with the signal input, though, doesn't require any tricks to remove TypeScript errors or warnings. This is made even more useful when you mark an input as required.
export class MyComponent {
@Input({ required: true }) name !: string;
firstName = input.required<string>();
}
The decorator input still needs the definite assignment assertion, even though we've marked it as required. This improvement to type safety is a much appreciated improvement to our components.
ngOnChanges
Another benefit of using input signals is that you no longer need to use the ngOnChanges lifecycle hook. This hook has its uses, but it's never been very developer friendly. It is triggered when the component is initialized and when an @Input is changed. You can then run some logic with the @Input of a component. Extracting the correct values though in the lifecycle hook was confusing and required several extra lines of code. With signals, though, you can use a computed signal, or an effect, and your component will automatically update if the signal input updates. Let's look at an example to clarify this. First is an example of a component with decorator inputs, second is an example of signal inputs.
export class UserProfileComponent implements OnChanges {
@Input() firstName = '';
@Input() lastName = '';
fullName = '';
ngOnChanges(changes: SimpleChanges): void {
// You have to check if the specific inputs changed
if (changes['firstName'] || changes['lastName']) {
this.fullName = `${this.firstName} ${this.lastName}`;
}
}
}
export class UserProfileComponent {
firstName = input('');
lastName = input('');
// Automatically updates whenever firstName or lastName changes
// No lifecycle hooks required
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}
The first example works fine, but it takes a lot more logic to get to the same point as the second example. Because of the way signals work, each time the firstName signal or lastName signal changes, the computed signal will automatically update, and our UI will reflect that change.
Read-only Inputs
Unlike decorator inputs, signal inputs are read-only. If your component needs to update the value of the input, which gives you two-way data binding between the components, you'll need to use the model signal. It's used in much the same way as a normal signal input:
export class CounterComponent {
count = model<number>(0);
increment() {
// Unlike input(), model() is writable
this.count.update(c => c + 1);
}
}
This will automatically provide a value change output that the parent component can use to get updates about the value. In this case, the output would be named countChange, and you would likely listen to the output with the familiar "banana-in-a-box" syntax:
<app-counter [(count)]="buttonClicks" />
When the CounterComponent updates the count signal, the parent component will update as well.
Data Transformation
Another very useful feature of signal inputs is the ability to automatically transform data as it comes in the component. The transformation occurs before the data is stored in the value of the signal. This is useful when the data comes into your component through an HTML attribute. The value of those attributes is often a string, even if your intention is for it to be a different type. For example, maybe you want it to be a boolean or a number. You can use a transform to convert the value for you, and the result will be stored in the signal. Let's look at an example.
<app-counter-display count="displayNumber" showDetails="true" />
In this case, the value of the count attribute in the child component would be a string instead of a number, and the value of showDetails would be the string 'true' rather than a boolean. We can coerce those values to the desired types with a transform:
export class CounterDisplay {
count = input<number>(0, { transform: numberAttribute })
showDetails = input<boolean>(false, { transform: booleanAttribute })
}
In this example, I'm using a couple of built in transform functions that convert an input to a number or boolean, respectively. If you need to write your own transform, provide a function that takes the value of the input as a parameter, and returns the value you want stored in the component.
Signal Outputs
Alright, you caught me. Technically speaking, there are no such things as signal outputs. But there is a new way to create outputs for your components that matches the style of using the signal inputs input() and model(). The output method returns an OutputEmitterRef, on which you still need to explicitly call .emit. You will also no longer need to instantiate an instance of EventEmitter when creating the output. Let's look at a couple of examples to demonstrate these differences.
export class UserCardComponent {
// Requires decorator + explicit class instantiation
@Output() deleteUser = new EventEmitter<string>();
updateUser = output<string>();
onDelete() {
this.deleteUser.emit('user-123');
}
onUpdate() {
this.updateUser.emit('user-123');
}
}
Again, the actual use of the output is essentially the same; the only real difference here is the instantiation of the two outputs. The parent would listen for the outputs in the same way as well.
<app-user-card (deleteUser)="deleteUser($event)" (updateUser)="updateUser($event)" />
Earlier we talked about the
modelsignal input, and how a value change output is automatically created. Angular essentially creates an output for you automatically in the same way that the demo is shown above.
outputFromObservable
One final convenience that is provided is a way to output a value from the observable inside a component. For example, let's say you have a SearchComponent that has a FormControl to do the search. Each time the value changes, you want to notify the parent of that change. Previously, you would have had to subscribe to the valueChanges observable of the FormControl and emit the new value on each change.
export class SearchComponent implements OnInit, OnDestroy {
// 1. Create the Output
@Output() query = new EventEmitter<string | null>();
searchControl = new FormControl('');
private querySub?: Subscription;
ngOnInit() {
// 2. Manually subscribe to the stream
this.querySub = this.searchControl.valueChanges.subscribe(value => {
// 3. Manually emit the value
this.query.emit(value);
});
}
ngOnDestroy() {
// 4. Manually clean up the subscription (The "Subscription Tax")
this.querySub?.unsubscribe();
}
}
All of this can be replaced with the following when we use the outputFromObservable method:
export class SearchComponent {
searchControl = new FormControl('');
// Automatically emits the value whenever the form control changes
query = outputFromObservable(this.searchControl.valueChanges);
}
Import
outputFromObservablefrom the@angular/core/rxjs-interoppackage.
This new feature simplifies our component drastically. We eliminate the lifecycle hooks and the manual subscription management, ensuring we never accidentally leave an open subscription that causes a memory leak. You don't need to use the new signal inputs to benefit from this convenience. If you have this use case, start using this new function right away.
Conclusion
Angular just keeps getting better and more developer friendly. At the same time our apps benefit from better performance, with very little effort required on our part.
The great thing about this specific feature is that you don't need to panic and update every component in your application. You can gradually update old components, while ensuring that new components use this new feature. As you go along, you'll eventually update your application. You can also use the signal input migration generator if you want to migrate everything all at once automatically. Whichever method works best for you is great.
Finally, by adopting these signal-based primitives now, you aren't just saving a few lines of code; you are future-proofing your application and preparing it for the era of zoneless performance.