How to Share Data Between Components in Angular (Signals & Services)

When building applications in Angular, sharing data between components is a fundamental requirement. Whether you need to pass an item down to a child card, notify a parent when a user clicks a button, or sync shopping cart state across unrelated header and sidebar components, Angular provides several distinct communication patterns.
⚡ Using Modern Angular (v17+)? Jump directly to Modern Angular: Signals
input(),output(),model(), and Signal Services to see the idiomatic, zero-leak approach.
In this guide, we will compare the classic patterns (@Input, @Output, BehaviorSubject) with the modern Angular primitives (input(), output(), model(), and Signal Services).
Let's start with direct Parent-to-Child and Child-to-Parent communication.
1. Parent-to-Child & Child-to-Parent Communication 🔄
When two components have a direct hierarchical relationship, passing data down and emitting events up is the cleanest approach.
Legacy Angular (v2 – v16): @Input() & @Output() Decorators
In older versions of Angular, we used decorators and EventEmitter:
// child.component.ts (Legacy)
import { Component, Input, Output, EventEmitter } from '@angular/core';
export interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-product-card',
template: `
<div class="product-card">
<h3>{{ product?.name }}</h3>
<p>\${{ product?.price }}</p>
<button (click)="selectProduct()">Select</button>
</div>
`
})
export class ProductCardComponent {
@Input() product?: Product;
@Output() selected = new EventEmitter<Product>();
selectProduct() {
if (this.product) {
this.selected.emit(this.product);
}
}
}Usage in the parent component template:
<app-product-card
[product]="currentProduct"
(selected)="onProductSelected($event)"
/>Now let's see how modern Angular simplifies this with Signals.
2. Modern Angular (v17+): input(), output(), and model() ⚡
Modern Angular replaces property decorators with reactive Signal functions.
Modern Child Component with Signal Inputs and Outputs
// child.component.ts (Modern Standalone)
import { Component, input, output } from '@angular/core';
export interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div class="product-card">
<h3>{{ product().name }}</h3>
<p>\${{ product().price }}</p>
<button (click)="selectProduct()">Select</button>
</div>
`
})
export class ProductCardComponent {
// 1. Required signal input (type-safe at compile time!)
readonly product = input.required<Product>();
// 2. Output function (no EventEmitter instantiation boilerplate)
readonly selected = output<Product>();
selectProduct() {
this.selected.emit(this.product());
}
}Two-Way Data Binding with model() (Angular 17.2+)
If you need two-way binding between parent and child (e.g. an expanded panel state or quantity counter), use model():
// quantity-picker.component.ts
import { Component, model } from '@angular/core';
@Component({
selector: 'app-quantity-picker',
standalone: true,
template: `
<div class="flex gap-2">
<button (click)="quantity.set(quantity() - 1)">-</button>
<span>{{ quantity() }}</span>
<button (click)="quantity.set(quantity() + 1)">+</button>
</div>
`
})
export class QuantityPickerComponent {
// Writable two-way signal!
readonly quantity = model(1);
}Parent template usage:
<!-- Automatically synchronizes both ways! -->
<app-quantity-picker [(quantity)]="itemCount" />Now, what if components are siblings or not directly connected in the template hierarchy? Let's explore Shared Services.
3. Cross-Component Communication with Services 🌐
When components live in separate branches of your application or across router outlets, passing inputs through multiple layers (prop drilling) becomes unmaintainable. An Injectable Service solves this cleanly.
Approach A: Legacy BehaviorSubject Service (RxJS)
// cart.service.ts (Legacy RxJS)
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly items$ = new BehaviorSubject<Product[]>([]);
readonly currentItems$ = this.items$.asObservable();
addItem(product: Product) {
const updated = [...this.items$.value, product];
this.items$.next(updated);
}
}In components, you had to manage subscriptions with async pipe or manually unsubscribe to prevent memory leaks:
<header>Cart Items: {{ (cartService.currentItems$ | async)?.length }}</header>Approach B: Modern Signal-Based State Service (Zero Leaks, No Async Pipe)
In modern Angular, Signal Services provide synchronous, readable state with zero subscription management:
// cart.service.ts (Modern Signal State)
import { Injectable, signal, computed } from '@angular/core';
import { Product } from './product.model';
@Injectable({ providedIn: 'root' })
export class CartService {
// 1. Private writable signal
private readonly itemsSignal = signal<Product[]>([]);
// 2. Public read-only signal
readonly items = this.itemsSignal.asReadonly();
// 3. Computed derived state (auto-recalculates when items change!)
readonly totalCount = computed(() => this.items().length);
readonly totalPrice = computed(() =>
this.items().reduce((acc, item) => acc + item.price, 0)
);
addItem(product: Product) {
this.itemsSignal.update((current) => [...current, product]);
}
removeItem(productId: number) {
this.itemsSignal.update((current) => current.filter((p) => p.id !== productId));
}
}Consuming the Signal Service in Any Component
// cart-badge.component.ts
import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';
@Component({
selector: 'app-cart-badge',
standalone: true,
template: `
<div class="badge">
🛒 {{ cart.totalCount() }} items (\${{ cart.totalPrice() }})
</div>
`
})
export class CartBadgeComponent {
readonly cart = inject(CartService);
}4. Querying Direct Child Components with viewChild() 🔍
When a parent component needs direct access to a child component's public methods or properties:
import { Component, viewChild, AfterViewInit } from '@angular/core';
import { ProductCardComponent } from './product-card.component';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [ProductCardComponent],
template: `<app-product-card #card [product]="featuredProduct" />`
})
export class ProductListComponent {
// Modern Signal-based viewChild (replaces @ViewChild decorator)
readonly card = viewChild.required<ProductCardComponent>('card');
focusCard() {
console.log('Accessing child component instance:', this.card());
}
}Communication Matrix: When to Use Which Pattern 📊
| Scenario | Recommended Pattern | Modern Angular Primitive |
|---|---|---|
| Parent to Child | One-way data flow | input() or input.required() |
| Child to Parent | Custom event notification | output() |
| Two-way synchronization | Mutual state sync | model() |
| Unrelated / Sibling Components | Shared singleton service | signal() + computed() service |
| Parent accessing Child methods | Direct component reference | viewChild() signal |
Recap 🛠️
To write clean, maintainable Angular applications:
- Use
input()andoutput()for standard parent-child communication. - Use
model()for two-way state synchronization without paired@Input/@Outputceremony. - Use Signal Services (
signal(),asReadonly(),computed()) to share global and cross-component state without memory leaks.
For more modern Angular architectural patterns, check out my articles on How to Simplify Component Communication with model() and Signals and How to Use ng-template, ng-container, and ng-content!
These are my experiences learning and facing my daily challenges working with Angular.
Frequently Asked Questions
What is the difference between legacy @Input() and modern Signal input() in Angular?
Legacy @Input() is a property decorator that relies on Zone.js and component lifecycle hooks to detect changes. Modern Signal input() (available in Angular 17.1+) returns a reactive Signal that integrates directly with computed() signals, enforces strict type safety with input.required(), and eliminates ChangeDetectionStrategy.OnPush edge cases.
How do you share two-way bound data between parent and child components in modern Angular?
Use the model() function (introduced in Angular 17.2+). It creates a writable signal in the child component that automatically synchronizes with a two-way [(property)] binding in the parent, eliminating the need to manually pair an @Input() with an @Output() EventEmitter.
When should I use a Service instead of Input/Output to share data in Angular?
Use Input/Output (or Signals input/output) for direct parent-child communication. When components are siblings, deeply nested across multiple router outlets, or need to share global application state, use an Injectable Service with Signals or RxJS.
How do Signal-based state services replace BehaviorSubject services in modern Angular?
Instead of creating a private BehaviorSubject with an asObservable() stream and managing manual subscriptions, you create a private signal() and expose it as a read-only asReadonly() signal. This gives components synchronous, zero-leak reactivity without needing async pipes or unsubscribe logic.
Related Articles
How to Simplify Angular Component Communication with model() and Signals
I was very busy in September, but now I'm working on creating a workshop about signals and developing ideas on how signals can simplify our code. Recently, I've been using @Input() and @Output for component communication. However, Angular 17 introduc...
How to Use ng-template, ng-container, and ng-content in Angular (Complete Guide)
Learn the exact differences between ng-template, ng-container, and ng-content in Angular, how to pass context with ngTemplateOutlet, multi-slot projection, and how they evolve with modern @if, @for, and @defer control flow.
How to Use Route and Query Parameters in Angular (with Signals & Inputs)
Learn how to pass and read route parameters, query parameters, and matrix parameters in Angular using Signals, withComponentInputBinding, and ActivatedRoute.
Share this article
If you found this guide helpful, consider sharing it with your team or fellow developers.
Real Software. Real Lessons.
I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.
Join 13,800+ developers and readers.
No spam ever. Unsubscribe at any time.