VOOZH about

URL: https://blog.logrocket.com/enhancing-two-way-data-binding-angular/

⇱ Enhancing two-way data binding in Angular - LogRocket Blog


2024-11-14
1828
#angular
Alexander Godwin
197878
110
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

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.

👁 Enhancing Two-Way Data Binding In Angular

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.

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

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:

  1. 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
  2. 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:
1. Easy declarations: With signals, there’s no need for separate @Input and @Output decorators for two-way binding
2. 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.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

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.

LogRocket understands everything users do in your Angular apps.

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 Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings—compatible with all frameworks.

With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.

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.

👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

Debug Next.js apps with AI agents and next-browser

Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.

👁 Image
Emmanuel John
Jun 17, 2026 ⋅ 9 min read

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

👁 Image
Chizaram Ken
Jun 16, 2026 ⋅ 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo — with email/password login, Google OAuth, session persistence, and protected routes.

👁 Image
Chinwike Maduabuchi
Jun 9, 2026 ⋅ 13 min read
View all posts

Would you be interested in joining LogRocket's developer community?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now