Chapter 11.3โ˜• 15 min read

Service-Based Communication

Services enable any component to communicate with any other component, regardless of their position in the component tree. No @Input drilling, no @Output chains.

01Why Service Communication

@Input/@Output works great for parent-child. But what about:

  • Siblings โ€” two components under the same parent?
  • Deeply nested โ€” grandchild to grandparent?
  • Unrelated โ€” sidebar and header that aren't parent-child?

"Service = WhatsApp group โ€” sab members share data, parent-child relation nahi chahiye."

The solution: A shared service injected into both components acts as a communication bridge. One component writes data to the service, the other reads it. No direct connection needed.

// Both components inject the same service
@Component({...}) export class MenuComponent {
  private cartService = inject(CartService);

  addToCart(item: MenuItem) {
    this.cartService.addItem(item);  // Write to service
  }
}

@Component({...}) export class CartBadgeComponent {
  private cartService = inject(CartService);

  // Read from service โ€” in template!
  count = this.cartService.itemCount;
}

Components don't need to know about each other. They only need to know about the service.

02BehaviorSubject Pattern (Old But Still Used)

The traditional approach uses BehaviorSubject in the service. This is still widely used in existing codebases.

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

export interface CartItem {
  id: number; name: string; price: number; quantity: number;
}

@Injectable({ providedIn: 'root' })
export class CartService {
  // Private Subject โ€” encapsulates write access
  private cartItemsSubject = new BehaviorSubject<CartItem[]>([]);

  // Public Observable โ€” read-only for consumers
  cartItems$: Observable<CartItem[]> = this.cartItemsSubject.asObservable();

  // Derived observable
  total$: Observable<number> = this.cartItems$.pipe(
    map(items => items.reduce((sum, i) => sum + i.price * i.quantity, 0))
  );

  count$: Observable<number> = this.cartItems$.pipe(
    map(items => items.length)
  );

  // Public methods to modify state
  addItem(item: CartItem) {
    this.cartItemsSubject.next([...this.cartItemsSubject.value, item]);
  }

  removeItem(id: number) {
    this.cartItemsSubject.next(
      this.cartItemsSubject.value.filter(i => i.id !== id)
    );
  }

  clearCart() {
    this.cartItemsSubject.next([]);
  }
}

"BehaviorSubject = shared whiteboard โ€” koi bhi likho, sab padh sakte ho. Lekin subscribe karna padta hai."

Consumer component (old way):

export class CartBadgeComponent implements OnInit, OnDestroy {
  private cartService = inject(CartService);
  count = 0;
  private sub!: Subscription;

  ngOnInit() {
    this.sub = this.cartService.count$.subscribe(c => this.count = c);
  }

  ngOnDestroy() {
    this.sub?.unsubscribe(); // Don't forget!
  }
}
03Signal Pattern (New Way)

The modern approach uses signal() in the service. Same result, simpler code.

import { Injectable, signal, computed } from '@angular/core';

export interface CartItem {
  id: number; name: string; price: number; quantity: number;
}

@Injectable({ providedIn: 'root' })
export class CartService {
  // Private writable signal
  private cartItems = signal<CartItem[]>([]);

  // Public readonly signal
  readonly items = this.cartItems.asReadonly();

  // Computed โ€” derived values
  readonly total = computed(() =>
    this.items().reduce((sum, i) => sum + i.price * i.quantity, 0)
  );

  readonly itemCount = computed(() => this.items().length);

  readonly formattedTotal = computed(() => `โ‚น${this.total()}`);

  // Public methods
  addItem(item: CartItem) {
    this.cartItems.update(items => [...items, item]);
  }

  removeItem(id: number) {
    this.cartItems.update(items => items.filter(i => i.id !== id));
  }

  updateQuantity(id: number, quantity: number) {
    this.cartItems.update(items =>
      items.map(i => i.id === id ? { ...i, quantity } : i)
    );
  }

  clearCart() {
    this.cartItems.set([]);
  }
}

"Signal = modern whiteboard โ€” same result, simpler code, no subscribe."

Consumer component (new way):

export class CartBadgeComponent {
  private cartService = inject(CartService);

  // Read directly โ€” no ngOnInit, no ngOnDestroy!
  itemCount = this.cartService.itemCount;
  total = this.cartService.total;
}

// Template: {{ itemCount() }} โ€” no async pipe needed!
04When to Use Service Communication

When to use service communication:

๐Ÿ’ก Use Service When...

  • Sibling components โ€” side-by-side components need to share data (cart + badge)
  • Deeply nested โ€” avoid "prop drilling" (@Input through 5+ levels)
  • Global state โ€” auth, notifications, theme, language settings
  • Unrelated components โ€” header, sidebar, router outlet that aren't in direct parent-child

"Prop drilling = parcel 5 logon se pass karna โ€” service = direct delivery."

Rule of thumb: If data passes through 3+ levels of @Input (GrandParent โ†’ Parent โ†’ Child), switch to a service. Beyond 2 levels, @Input becomes "drilling" and is hard to maintain.

// โŒ Bad: Prop drilling through 4 levels
<grandparent [user]="user">
  <parent [user]="user">
    <child [user]="user">
      <grandchild [user]="user">  โ† Why does parent need user?
        {{ user.name }}
      </grandchild>
    </child>
  </parent>
</grandparent>

// โœ… Good: Service injected where needed
<grandchild>{{ authService.user()?.name }}</grandchild>
05Anti-Patterns to Avoid

Common anti-patterns to avoid:

1. Exposing writable service state directly

// โŒ WRONG โ€” any component can corrupt state
cartItems = signal<CartItem[]>([]);  // Public AND writable!

// โœ… CORRECT โ€” read-only exposed
private cartItems = signal<CartItem[]>([]);
readonly items = this.cartItems.asReadonly();

2. Using service for simple parent-child data

// โŒ WRONG โ€” @Input is simpler for direct parent-child
// Child component using service just to get data from parent

// โœ… CORRECT โ€” @Input for one-level parent-child
@Input() biryaniName = '';

3. Putting component-specific state in service

// โŒ WRONG โ€” form data belongs to component
// @Injectable() export class FormService {
//   formData = signal({ name: '', email: '' });  // Component-specific!
// }

// โœ… CORRECT โ€” form state in component
export class FormComponent {
  formData = signal({ name: '', email: '' });  // Local state
}

4. Creating service for one-time parent-child

// โŒ WRONG โ€” over-engineering
// @Injectable() export class OneTimeDataService { ... }
// Use @Input instead for one-level communication

// โœ… Use @Input for direct, @Output for events, service for multi-level

"Service = public place โ€” sirf shared data rakho, personal data nahi."

Key Takeaways

  • โœ… Services enable any component to communicate โ€” no parent-child needed
  • โœ… Use for: siblings, deeply nested, global state, unrelated components
  • โœ… Signal pattern: signal() + asReadonly() + computed() โ€” no subscribe
  • โœ… Always expose asReadonly() or asObservable() โ€” don't leak write access
  • โœ… Don't use service for simple parent-child โ€” @Input is simpler
  • โœ… Don't put component-specific state in shared services
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