Chapter 11.4โ˜• 15 min read

Signal-Based Communication (Modern Way)

Signals are the future of Angular component communication. Combine input(), output(), model(), and service signals for a fully reactive, boilerplate-free codebase.

01The Signal Communication Pattern

Signals aren't just for local state โ€” they transform how components communicate. The combination of input(), output(), model(), and service signals creates a fully reactive communication system with minimal boilerplate.

"Signals se communication = apna walkie-talkie โ€” simple, fast, no subscribe."

The signal communication stack:

  • input() โ€” @Input as a signal (parent โ†’ child)
  • output() โ€” @Output as an Observable (child โ†’ parent)
  • model() โ€” Two-way binding with signals (โ†”)
  • Service with signal() โ€” siblings / deeply nested / global state
// The signal communication stack in action
@Component({...})
export class ChildComponent {
  // Signal input โ€” read with name()
  name = input.required<string>();

  // Signal output โ€” emit events
  select = output<Biryani>();

  // Model โ€” two-way binding
  quantity = model(1);
}

Current status: model() is stable. input() and output() are available in Angular 17.3+ (input() is stable, output() is stable). Use them in new projects!

02Signal Input (Angular 17.3+)

input() is the signal-based replacement for @Input() decorator. It's available in Angular 17.3+.

import { Component, input } from '@angular/core';

@Component({...})
export class BiryaniCardComponent {
  // Required input โ€” parent MUST provide
  name = input.required<string>();

  // Optional input with default
  price = input(0);

  // Optional with transform
  category = input('Non-Veg', { alias: 'cat' });

  // Read the value like any signal
  formattedPrice = computed(() => `โ‚น${this.price()}`);
}

"input() = @Input ka signal version โ€” future standard."

Benefits over @Input:

  • Value is a signal โ€” use in computed(), effect(), model()
  • No ! non-null assertion needed
  • No ngOnChanges needed โ€” use effect() or computed()
  • Same template syntax: <app-card [name]="value">
  • input.required() โ€” compile error if parent doesn't provide
// OLD: need ngOnChanges to detect @Input changes
@Input() name = '';
ngOnChanges(changes: SimpleChanges) {
  if (changes['name']) { /* react */ }
}

// NEW: just use computed or effect
name = input.required<string>();
greeting = computed(() => `Hello ${this.name()}!`);  // Auto-reactive!
effect(() => console.log('Name changed:', this.name()));  // Auto-tracked!
03Signal Output (Angular 17.3+)

output() is the signal-based replacement for @Output() decorator. Available in Angular 17.3+.

import { Component, output } from '@angular/core';

@Component({...})
export class BiryaniCardComponent {
  // Signal output โ€” emits values
  orderPlaced = output<Order>();
  favToggled = output<boolean>();

  placeOrder() {
    // .emit() works just like EventEmitter
    this.orderPlaced.emit({ name: this.name(), qty: 1, price: this.price() });
  }

  toggleFav() {
    this.favToggled.emit(!this.isFav);
  }
}

// Parent uses the same syntax:
// <app-card (orderPlaced)="onOrder($event)" (favToggled)="onFav($event)">

"output() = @Output ka modern version โ€” same EventEmitter API under the hood, but cleaner."

Benefits over @Output:

  • No = new EventEmitter() boilerplate
  • TypeScript infers the type โ€” no generic needed on right side
  • Same template binding syntax โ€” (orderPlaced)="handler()"
  • Works with outputFromObservable() for RxJS-to-output conversion
// OLD
@Output() orderPlaced = new EventEmitter<Order>();

// NEW โ€” same result, less code
orderPlaced = output<Order>();

// Advanced: output from RxJS Observable
import { outputFromObservable } from '@angular/core/rxjs-interop';
results$ = outputFromObservable(this.searchResults$.pipe(debounceTime(300)));
04Complete Signal Communication Example

Here's a complete example combining all signal communication patterns:

// โ”€โ”€โ”€ SERVICE (shared state) โ”€โ”€โ”€
@Injectable({ providedIn: 'root' })
export class BiryaniService {
  // Signal for shared state
  private selectedItems = signal<Biryani[]>([]);
  readonly items = this.selectedItems.asReadonly();

  selectItem(item: Biryani) {
    this.selectedItems.update(items => [...items, item]);
  }

  clearSelection() {
    this.selectedItems.set([]);
  }
}

// โ”€โ”€โ”€ CHILD COMPONENT (input + output) โ”€โ”€โ”€
@Component({
  selector: 'app-biryani-item',
  template: `
    <div class="item" [class.selected]="selected()">
      <h3>{{ biryani().name }}</h3>
      <p>โ‚น{{ biryani().price }}</p>
      <input [model]="quantity" type="number" min="1">
      <button (click)="addToCart()">๐Ÿ›’ Add</button>
    </div>
  `
})
export class BiryaniItemComponent {
  // Signal inputs
  biryani = input.required<Biryani>();
  selected = input(false);

  // Model for two-way
  quantity = model(1);

  // Output
  add = output<{ biryani: Biryani; quantity: number }>();

  addToCart() {
    this.add.emit({ biryani: this.biryani(), quantity: this.quantity() });
    this.quantity.set(1); // Reset
  }
}

// โ”€โ”€โ”€ PARENT COMPONENT โ”€โ”€โ”€
@Component({
  template: `
    <app-biryani-item
      [biryani]="currentBiryani"
      [(quantity)]="currentQty"
      (add)="onAddToCart($event)" />

    <p>Cart items: {{ biryaniService.items().length }}</p>
  `
})
export class ParentComponent {
  currentBiryani = signal({ name: 'Hyderabadi', price: 250 });
  currentQty = signal(1);
  biryaniService = inject(BiryaniService);

  onAddToCart(event: { biryani: Biryani; quantity: number }) {
    this.biryaniService.selectItem(event.biryani);
  }
}

"Mix of signals + input/output/model() = current best practice for new Angular apps."

05Signal Communication Decision Guide

A quick decision guide for signal-based communication:

ScenarioNow (Angular 17.3+)Notes
Parent โ†’ Child (data)input()input.required() for mandatory
Child โ†’ Parent (event)output()Clean EventEmitter replacement
Two-way bindingmodel()Stable, recommended
Siblings / DeepService with signal()asReadonly() for encapsulation
Local component statesignal()Simple and reactive
Derived statecomputed()Cached, auto-updating

"Abhi mix use karo โ€” signals jahan possible, @Input/@Output jahan zaroori ho."

Migration path for existing apps:

  1. Start using model() for all new two-way bindings
  2. Use output() for new output events (it's stable)
  3. Use input() for new inputs (stable in Angular 17.3+)
  4. Convert service BehaviorSubjects to signal() + asReadonly()
  5. Keep old @Input/@Output working โ€” migrate component by component

Key Takeaways

  • โœ… input() = signal-based @Input โ€” use computed()/effect() to react to changes
  • โœ… output() = signal-based @Output โ€” cleaner EventEmitter
  • โœ… model() for two-way binding โ€” stable, recommended for all new code
  • โœ… Service with signal + asReadonly for sibling/deep communication
  • โœ… Mix is okay โ€” use signals where possible, keep @Input/@Output as needed
  • โœ… input() and output() are stable in Angular 17.3+
Course Search
Search across all chapters & stages
๐Ÿ“–

Search the course

Type any topic โ€” branching, stash, rebase, hooks โ€” and jump straight to that chapter.

merge branchesgit stashundo commitrebase