How to Share Data Between Sibling Components

Let me share my approach to implementing component communication in Angular, particularly between sibling components. I’ll demonstrate this using a real-world example from an e-commerce dashboard.

There are several methods I use depending on the scenario:

  1. Shared Service with Signals
  2. State Management (NgRx/Signal Store)
  3. Event Bus Pattern
  4. Parent Component Communication

Here’s how I implement each approach:

// 1. Shared Service with Signals
@Injectable({ providedIn: 'root' })
export class SharedDataService {
  // State management using signals
  private cartItems = signal<CartItem[]>([]);
  private wishlistItems = signal<Product[]>([]);
  
  // Computed values
  totalCartItems = computed(() => 
    this.cartItems().reduce((sum, item) => sum + item.quantity, 0)
  );
  
  wishlistCount = computed(() => 
    this.wishlistItems().length
  );
  
  // Methods to update state
  addToCart(item: CartItem) {
    this.cartItems.update(items => [...items, item]);
  }
  
  addToWishlist(product: Product) {
    this.wishlistItems.update(items => [...items, product]);
  }
}

// 2. Cart Component (Sibling 1)
@Component({
  selector: 'app-cart',
  template: `
    <div class="cart">
      <h2>Cart ({{ shared.totalCartItems() }})</h2>
      @for (item of cartItems(); track item.id) {
        <cart-item 
          [item]="item"
          (remove)="removeItem($event)"
        />
      }
    </div>
  `
})
export class CartComponent {
  private shared = inject(SharedDataService);
  
  // Expose signals to template
  cartItems = this.shared.cartItems;
  
  removeItem(itemId: string) {
    this.shared.removeFromCart(itemId);
  }
}

// 3. Wishlist Component (Sibling 2)
@Component({
  selector: 'app-wishlist',
  template: `
    <div class="wishlist">
      <h2>Wishlist ({{ shared.wishlistCount() }})</h2>
      @for (product of wishlistItems(); track product.id) {
        <product-card 
          [product]="product"
          (addToCart)="moveToCart($event)"
        />
      }
    </div>
  `
})
export class WishlistComponent {
  private shared = inject(SharedDataService);
  
  // Expose signals to template
  wishlistItems = this.shared.wishlistItems;
  
  moveToCart(product: Product) {
    this.shared.addToCart({
      id: product.id,
      quantity: 1,
      price: product.price
    });
    this.shared.removeFromWishlist(product.id);
  }
}

// 4. Event Bus Pattern
@Injectable({ providedIn: 'root' })
export class EventBusService {
  private events = new Subject<EventData>();
  
  emit(event: EventData) {
    this.events.next(event);
  }
  
  on(eventType: string): Observable<EventData> {
    return this.events.pipe(
      filter(event => event.type === eventType)
    );
  }
}

// 5. Using Event Bus
@Component({
  selector: 'app-product-list',
  template: `
    @for (product of products(); track product.id) {
      <product-card 
        [product]="product"
        (addToCart)="notifyProductAdded($event)"
      />
    }
  `
})
export class ProductListComponent {
  private eventBus = inject(EventBusService);
  
  notifyProductAdded(product: Product) {
    this.eventBus.emit({
      type: 'PRODUCT_ADDED',
      payload: product
    });
  }
}

// 6. Header Component (Listening to Events)
@Component({
  selector: 'app-header',
  template: `
    <header>
      <cart-icon [count]="cartCount()" />
      <notifications [message]="latestNotification()" />
    </header>
  `
})
export class HeaderComponent implements OnInit {
  private eventBus = inject(EventBusService);
  private destroy$ = inject(DestroyRef);
  
  cartCount = signal(0);
  latestNotification = signal<string | null>(null);
  
  ngOnInit() {
    // Listen to product added events
    this.eventBus.on('PRODUCT_ADDED').pipe(
      takeUntilDestroyed(this.destroy$)
    ).subscribe(event => {
      this.cartCount.update(count => count + 1);
      this.showNotification(
        `Added ${event.payload.name} to cart`
      );
    });
  }
  
  private showNotification(message: string) {
    this.latestNotification.set(message);
    setTimeout(() => {
      this.latestNotification.set(null);
    }, 3000);
  }
}

Real-world example of component communication:

// Before: Tightly coupled components
@Component({
  template: `
    <div>
      {{ data }}
      <button (click)="updateSibling()">Update</button>
    </div>
  `
})
class ComponentA {
  @Output() update = new EventEmitter<string>();
  
  updateSibling() {
    this.update.emit('new data');
  }
}

// After: Using shared service and signals
@Injectable({ providedIn: 'root' })
class SharedState {
  private state = signal<AppState>({
    data: null,
    loading: false,
    error: null
  });
  
  // Computed values
  data = computed(() => this.state().data);
  loading = computed(() => this.state().loading);
  error = computed(() => this.state().error);
  
  // Actions
  updateData(newData: any) {
    this.state.update(state => ({
      ...state,
      data: newData
    }));
  }
}

@Component({
  template: `
    @if (state.loading()) {
      <loading-spinner />
    }
    
    @if (state.data(); as data) {
      <data-display [data]="data" />
    }
  `
})
class ComponentB {
  state = inject(SharedState);
}

Key points I emphasize in interviews:

  1. Service-based Communication

    • Centralized state management
    • Reactive updates with signals
    • Clean component separation
  2. Event Bus Pattern

    • Decoupled communication
    • Type-safe events
    • Easy to debug
  3. State Management

    • Predictable updates
    • Single source of truth
    • Performance optimization
  4. Best Practices

    • Unsubscribe from observables
    • Use proper typing
    • Implement error handling
    • Maintain clean architecture

This approach has helped me build maintainable applications with clear communication patterns between components.

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.

Test Your Angular Knowledge

Ready to put your skills to the test? Take our interactive Angular quiz and get instant feedback on your answers.