Written by Alexander Godwin✏️
Two-way data binding in Angular, traditionally achieved through @Input
and @Output
decorators to synchronize data between parent and child components, has taken a significant leap forward with the introduction of signals — a new reactive primitive in Angular's ecosystem.
In this article, we’ll take a closer look at signals so that you can consider a more effective approach for future projects. First, we’ll examine the conventional way of achieving two-way data binding in Angular. Then we’ll explore just how and why signals, with their intuitive coding and brevity, are a stronger alternative to the traditional approach.
Two-way data binding with @input
and @output
decorators
Every frontend framework has its own way of handling two-way data binding in Angular. The conventional approach is to use the @input
and @output
decorators to achieve a bi-directional data flow between parent and child components.
The @input
decorator is responsible for passing data from parent to child components. In simpler words, it allows child components to receive and use data from parent components.
Meanwhile, the @output
decorator emits events from a child component to its parent. This usually works through EventEmitter
being sourced in the child component, which in turn enables the data to be emitted to the parent component.
The code snippets below demonstrate how to use the @input
and @output
decorators to achieve two-way data binding:
// counter.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{ count }}</span>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
@Input() count: number;
@Output() countChange = new EventEmitter<number>();
increment() {
this.count++;
this.countChange.emit(this.count);
}
decrement() {
this.count--;
this.countChange.emit(this.count);
}
}
In the code snippet above, the CounterComponent
receives an initial count
value through @Input
and emits changes through the countChange
@Output
.
We can use the component above in a parent component, as follows:
// app.component.ts
import { Component } from '@angular/core';
import { CounterComponent } from './counter/counter.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [CounterComponent],
template: `
<main>
<app-counter [(count)]="parentCount"></app-counter>
</main>
`,
})
export class AppComponent {
parentCount = 30;
}
What are signal-based inputs?
One of the most significant changes to the Angular ecosystem is the introduction of signal-based inputs. This new method of data handling and reactivity has the potential to improve framework performance even more.
What are signals?
Signals are new primitives introduced as a means to achieve reactivity in Angular. Though they may appear similar to observables, there are some key differences:
- Simplicity: Unlike observables, signals are more to the point and less cumbersome
- Fine-grained reactivity: Signal updates are more fine-tuned, which increases efficiency
import { signal } from '@angular/core';
// Create a signal
const count = signal(0);
// Read the value
console.log(count()); // Output: 0
// Update the value
count.set(5);
console.log(count()); // Output: 5
// Update based on previous value
count.update(value => value + 1);
console.log(count()); // Output: 6
Signals in components In components, signals can be used to create reactive properties:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{ count() }}</span>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
}
Why do we need signals?
With Angular’s evolution across its development, signals have become the best choice for two-way data binding.
There are a couple of reasons why we should use signals over the traditional approach:
- Performance: Signals can cause an improvement in change detection and rendering
- Simplicity: They offer a simpler technique for managing reactive states
- Consistency: Signals may offer a better approach to fluid state management within any application
Comparing inputs with signals to the traditional approach
Signal-based inputs bring significant advantages in comparison to the synchronized approaches of the @Input
and @Output
decorators. Let's examine some of these upsides.
Enhanced performance
One of the things developers appreciate most about signals is the improvement of performance they bring. Here are two ways that signals carry out this improvement:
- Fine-grained updates: Signals minimize the chances of unnecessary re-renders by allowing for more precise updates. Only components that depend on a signal that changes will be triggered for updates enhancing change detection
- Reduced change detection cycles: With signals, Angular can also optimize its change detection, focusing only on the changes in the application with the help of signals
// Traditional approach
@Component({
selector: 'app-child',
template: `<p>{{data}}</p>`
})
export class ChildComponent {
@Input() data: string;
}
// Signal-based approach
@Component({
selector: 'app-child',
template: `<p>{{data()}}</p>`
})
export class ChildComponent {
data = input<string>();
}
Simpler syntax and reduced boilerplate
Signal-based inputs simplify component communication in two ways:
-
Easy declarations: With signals, there’s no need for separate
@Input
and @Output decorators for two-way binding - Better updates: Updating a signal is straightforward, without the need for EventEmitters
// Traditional approach
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{count}}</span>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
@Input() count: number = 0;
@Output() countChange = new EventEmitter<number>();
increment() {
this.count++;
this.countChange.emit(this.count);
}
decrement() {
this.count--;
this.countChange.emit(this.count);
}
}
// Signal-based approach
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{count()}}</span>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = input<number>(0);
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
}
Better type inference
Signals provide improved type safety and inference:
- Automatic type inference: More often than not, TypeScript infers the types of signals automatically and reduces the need for explicit type declarations
- Consistent types: The signal’s type is consistent throughout its lifecycle, removing potential type mismatches between input and output
Easier testing and debugging Signal-based inputs simplify the testing and debugging process by allowing for:
- Predictable state changes: Signals make state changes more predictable and easier to track
- Simplified mocking: In unit tests, mocking signal inputs is often simpler than mocking
@Input
and@Output
combinations
Enhanced reactivity
Signals also provide a more reactive programming model. They do this through:
- Derived signals: With signals, we can easily create computed values that automatically update when their dependencies change
- Effect management: We can use
effect()
to react to signal changes more concisely than with traditional change detection
// derived signal example
@Component({
selector: 'app-derived-example',
template: `<p>Double count: {{doubleCount()}}</p>`
})
export class DerivedExampleComponent {
count = signal(5);
doubleCount = computed(() => this.count() * 2);
}
Demos: Practically implementing signal-based inputs
Basic example — converting a simple @input
/@output
component to use signals:
import { Component, Input, Output, EventEmitter, signal, computed, input } from '@angular/core';
// Traditional approach
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{count}}</span>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
@Input() count: number = 0;
@Output() countChange = new EventEmitter<number>();
increment() {
this.count++;
this.countChange.emit(this.count);
}
decrement() {
this.count--;
this.countChange.emit(this.count);
}
}
import { Component, Input, Output, EventEmitter, signal, computed, input } from '@angular/core';
// Signal-based approach
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{count()}}</span>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = input<number>(0);
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
}
Advanced example — implementing a form with multiple inputs using signals:
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
template: `
<div>
<label for="name">Name:</label>
<input id="name" type="text" [value]="name()" (input)="updateName($event)" />
</div>
<div>
<label for="age">Age:</label>
<input id="age" type="number" [value]="age()" (input)="updateAge($event)" />
</div>
<p>Your name is: {{ name() }}</p>
<p>Your age is: {{ age() }}</p>
`
})
export class TwoWayBindingComponent {
// Define signals for name and age
name = signal<string>('');
age = signal<number | null>(null);
// Update signals when user inputs data
updateName(event: Event) {
const inputElement = event.target as HTMLInputElement;
this.name.set(inputElement.value);
}
updateAge(event: Event) {
const inputElement = event.target as HTMLInputElement;
this.age.set(Number(inputElement.value));
}
}
Usage in both parent and child components:
//main.ts
import { ChildComponent } from "./counter.component"
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h2>Parent Component</h2>
<p>Count: {{ count() }}</p>
<p>Doubled Count: {{ doubledCount() }}</p>
<button (click)="increment()">Increment</button>
<app-child [parentCount]="count" (updateCount)="updateCount($event)"></app-child>
`,
standalone: true,
imports: [ChildComponent]
})
export class ParentComponent {
count = signal(0);
doubledCount = computed(() => this.count() * 2);
increment() {
this.count.update(n => n + 1);
}
updateCount(newValue: number) {
this.count.set(newValue);
}
}
// child.component.ts
import { CommonModule } from '@angular/common';
import { Component, input, output } from '@angular/core';
import { type Signal } from '@angular/core';
@Component({
selector: 'app-child',
template: `
<h3>Child Component</h3>
<p>Parent Count: {{ parentCount() }}</p>
<button (click)="multiplyByTwo()">Multiply by 2</button>
`,
standalone: true,
imports: [CommonModule]
})
export class ChildComponent {
parentCount = input.required<Signal<number>>();
updateCount = output<number>();
multiplyByTwo() {
const currentValue = this.parentCount()();
this.updateCount.emit(currentValue * 2);
}
}
The examples above prove that signal-based inputs can be used in different cases, from simple components to more complex forms. Signals can be key to improving performance in cases with frequent updates.
Conclusion
In this article, we've looked at how two-way data binding in Angular has changed over time, moving from the conventional @Input
and @Output
decorators to the more recent signal-based method. Let's review our main ideas:
- Traditional approach: To begin, we reviewed the conventional approach of two-way data binding using
@Input
and@Output
decorators - Introduction to signals: Next, we covered the new Angular primitive called signals, which can be used to control reactive states. Signals provide a more straightforward and user-friendly syntax for managing data transfer among components
- Benefits of signal-based inputs: We outlined several advantages of signal usage, including better performance, simpler syntax, better type inference, easier testing, and enhanced reactivity
- Practical implementations: We looked at how signal-based inputs can be used in real-world settings, ranging from basic counter components to more intricate forms and high-frequency update scenarios
These signal-based inputs are a major advancement in component communication and state management. Signals provide a convenient and developer-friendly alternative to the @Input
/@Output
technique, so keep them in mind as you start new projects or restructure existing ones.
Experience your Angular apps exactly how a user does
Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.
Modernize how you debug your Angular apps — start monitoring for free.
Top comments (1)
Absolutely incredible! 😻.