Component Communication
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Angular Interview Questions · Component Communication Companion Article
Angular applications are built from many components. A small application may contain only a few components, while a real production application can contain hundreds of components arranged in parent-child trees. These components cannot work completely independently. They frequently need to exchange data, notify each other about user actions, or share application state. Angular provides several communication mechanisms for different situations: Inputs for sending data from a parent to a child Outputs for sending events from a child to a parent Model inputs for two-way component communication Component references for directly accessing a component instance Services for communication between components that do not have a simple parent-child relationship Signals for reactive state communication Choosing the correct communication mechanism is important because it affects readability, maintainability, reusability, and application architecture.
Before choosing an Angular API, identify where the information starts and where it needs to go.
A typical component hierarchy may look like this:
AppComponent
│
├── HeaderComponent
│
├── ProductListComponent
│ ├── ProductCardComponent
│ ├── ProductCardComponent
│ └── ProductCardComponent
│
└── CartComponent
Different communication requirements need different solutions.
For example:
ProductListComponent
|
| product
↓
ProductCardComponent
The parent sends product information to the child.
This is normally handled using an input.
Now consider:
ProductListComponent
↑
| addToCart event
|
ProductCardComponent
The child informs the parent that the user clicked Add to Cart.
This is normally handled using an output.
If multiple unrelated components need access to the cart state:
ProductCardComponent
|
↓
CartService
↑
|
CartComponent
A shared service can provide a cleaner solution.
An input property allows a component to receive information from its parent.
The general communication direction is:
Parent → Child
Suppose a parent component contains a username:
export class ParentComponent {
username = 'Rahul';
}
The child can declare an input:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user',
template: `<h2>{{ name }}</h2>`
})
export class UserComponent {
@Input() name = '';
}
The parent passes its value using property binding:
<app-user [name]="username"></app-user>
Angular evaluates username in the parent and provides the resulting value to the child's name property.
A value without square brackets is generally treated as literal text:
<app-user name="Rahul"></app-user>
Property binding evaluates an Angular expression:
<app-user [name]="username"></app-user>
This distinction matters.
For example:
<app-product price="500"></app-product>
passes a text value.
While:
<app-product [price]="500"></app-product>
passes the evaluated numeric expression.
Inputs are not limited to strings and numbers.
A complete object can be provided:
product = {
id: 101,
name: 'Laptop',
price: 65000
};
Parent template:
<app-product-card [product]="product"></app-product-card>
Child:
@Input() product!: {
id: number;
name: string;
price: number;
};
Passing structured objects is common when creating reusable UI components such as:
Modern Angular provides the input() function as an alternative to decorator-based @Input() declarations.
A component can declare:
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user',
template: `<h2>{{ name() }}</h2>`
})
export class UserComponent {
name = input('');
}
The parent still uses normal property binding:
<app-user [name]="username"></app-user>
The important difference is that input() produces a signal-based input.
Therefore, its value is read by calling it:
this.name()
instead of:
this.name
Angular's current input() API supports optional inputs and required inputs.
Sometimes a child component cannot work correctly without a particular value.
For example, a product details component may require a product ID.
It can be declared as:
productId = input.required<number>();
Parent:
<app-product-details
[productId]="selectedProductId"
></app-product-details>
Required inputs make the component contract clearer.
Instead of silently allowing an important value to be missing, the component explicitly states that the consumer must provide it.
An input may sometimes need normalization before the component uses it.
For example, a component may receive configuration values that need conversion or transformation.
Keeping input normalization close to the input boundary can make the rest of the component simpler because its internal logic works with predictable values.
The general design principle is:
External value
↓
Input boundary
↓
Validation / transformation
↓
Internal component logic
Inputs solve parent-to-child communication.
For the opposite direction, Angular provides outputs.
The communication direction becomes:
Child → Parent
The child does not normally modify the parent's state directly.
Instead, it announces that something happened.
Typical child events include:
save
cancel
delete
selected
submitted
closed
quantityChanged
itemAdded
This event-based design keeps the child component reusable.
A traditional Angular output uses @Output() and EventEmitter.
Example:
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'app-product-card',
template: `
<button (click)="addProduct()">
Add to Cart
</button>
`
})
export class ProductCardComponent {
@Output() addToCart = new EventEmitter<number>();
addProduct(): void {
this.addToCart.emit(101);
}
}
Parent:
<app-product-card
(addToCart)="handleAddToCart($event)">
</app-product-card>
Parent class:
handleAddToCart(productId: number): void {
console.log('Product selected:', productId);
}
$event represents the value emitted by the child.
Modern Angular also provides the output() function for declaring component outputs. Angular documents output() as the newer output API, and parents can listen to these outputs through normal template event bindings.
Example:
import { Component, output } from '@angular/core';
@Component({
selector: 'app-product-card',
template: `
<button (click)="selectProduct()">
Select
</button>
`
})
export class ProductCardComponent {
selected = output<number>();
selectProduct(): void {
this.selected.emit(101);
}
}
Parent:
<app-product-card
(selected)="onProductSelected($event)">
</app-product-card>
Parent class:
onProductSelected(productId: number): void {
console.log(productId);
}
The communication flow remains:
User Action
↓
Child Component
↓
output.emit(value)
↓
Parent Event Binding
↓
Parent Handler
A useful naming convention is to describe what happened.
Good examples:
saved = output<void>();
productSelected = output<Product>();
quantityChanged = output<number>();
dialogClosed = output<void>();
Avoid designing an output like a command sent upward.
Components become easier to understand when outputs represent events rather than instructions to the parent.
Parent-to-child communication is one of the most common Angular patterns.
Consider an ecommerce application.
The parent has:
selectedProduct = {
id: 10,
name: 'Keyboard',
price: 2500
};
The child declares:
product = input.required<Product>();
Parent:
<app-product-details
[product]="selectedProduct">
</app-product-details>
Child:
<h2>{{ product().name }}</h2>
<p>₹{{ product().price }}</p>
The parent owns the data.
The child receives the data and decides how to display or use it.
This creates a predictable one-way flow:
Parent State
↓
Input Binding
↓
Child Component
↓
Rendered UI
One-way flow is valuable because the owner of the state remains easy to identify.
Consider a product card containing an Add to Cart button.
The product card should not need to understand the complete shopping-cart implementation.
Instead, it emits an event.
Child:
productAdded = output<number>();
addToCart(): void {
this.productAdded.emit(this.product().id);
}
Parent:
<app-product-card
[product]="product"
(productAdded)="addProductToCart($event)">
</app-product-card>
Parent:
addProductToCart(productId: number): void {
// Update cart state
}
This produces good separation of responsibilities.
The child knows:
"The user requested this product."
The parent decides:
"What should happen when the product is requested?"
The same child could therefore be reused in different contexts.
Sometimes communication needs to work in both directions.
For example:
Parent → Child
Parent ← Child
A reusable counter might receive its current value from the parent but also modify that value when its buttons are clicked.
Modern Angular provides model inputs for this pattern. A model input is writable and Angular automatically creates a corresponding change output for it.
A model input is declared using model().
Example:
import { Component, model } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="decrease()">-</button>
<span>{{ count() }}</span>
<button (click)="increase()">+</button>
`
})
export class CounterComponent {
count = model(0);
increase(): void {
this.count.update(value => value + 1);
}
decrease(): void {
this.count.update(value => value - 1);
}
}
The parent can use:
<app-counter [(count)]="quantity"></app-counter>
This creates two-way component binding.
Angular's model input automatically has a matching output whose name uses the Change suffix. For example, a count model corresponds to countChange.
Conceptually:
count input
+
countChange output
=
[(count)]
A model can also represent a value that the parent must provide.
For example:
checked = model.required<boolean>();
This is useful for reusable controls where a parent-owned value must always exist.
Angular demonstrates this pattern for two-way binding with model signals.
Angular uses the [()] syntax for two-way binding.
It combines:
[property]
with:
(event)
giving:
[(property)]
Angular documentation commonly refers to this syntax as banana-in-a-box.
Consider:
quantity = 1;
Parent template:
<app-quantity-selector
[(quantity)]="quantity">
</app-quantity-selector>
<p>Selected quantity: {{ quantity }}</p>
Child:
quantity = model(1);
When the child updates:
this.quantity.update(value => value + 1);
the parent value is also updated.
Two-way binding is particularly useful for components representing editable values.
Examples include:
For ordinary notification events, an output is often clearer.
For example, this:
deleted = output<number>();
better represents a delete event than attempting to model deletion as two-way state.
Sometimes a parent needs direct access to a component or template element.
Angular template reference variables provide one way to create a reference.
Example:
<app-video-player #player></app-video-player>
<button (click)="player.play()">
Play
</button>
Here:
#player
creates a template reference to the component instance.
The template can access public properties or methods exposed by that component.
Template references can also point to native elements.
Example:
<input #searchInput>
<button (click)="search(searchInput.value)">
Search
</button>
The reference gives the template access to the input element.
Component references can be appropriate for imperative UI actions such as:
However, direct references should not replace normal data flow everywhere.
If a parent only needs to provide data, an input communicates the intention more clearly.
If a child only needs to report an event, an output is usually clearer.
Signals provide reactive values whose consumers can react when those values change. Angular tracks where signal state is consumed so that relevant state changes can update the application appropriately.
Signals can participate in component communication in several ways.
name = input('');
Useful for:
Parent → Child
quantity = model(1);
Useful for:
Parent ↔ Child
private readonly user = signal<User | null>(null);
Useful for:
Component
↓
Shared Service
↑
Other Components
Consider an application containing:
ProductPageComponent
|
↓
ProductCardComponent
|
↓
CartService
|
↓
HeaderComponent
The product page provides product information:
<app-product-card
[product]="selectedProduct"
(viewDetails)="openDetails($event)">
</app-product-card>
The child receives the product:
product = input.required<Product>();
The child reports an action:
viewDetails = output<number>();
A separate cart service manages global cart state:
readonly cartCount = signal(0);
The header reads the shared state:
Cart ({{ cartService.cartCount() }})
Different mechanisms solve different communication problems.
Trying to use one mechanism for every situation usually creates unnecessary complexity.
Use this practical decision guide.
| Requirement | Suitable Approach |
|---|---|
| Parent provides data to child | input() / input property |
| Child reports an action to parent | output() / output event |
| Parent and child edit the same component value | model() |
| Template needs direct component access | Template reference |
| Distant components share application state | Shared service |
| Shared state should react automatically | Signals in a service |
| Child requires a value | input.required() |
| Child requires two-way model state | model.required() |
The relationship between components should determine the communication method.
These APIs solve different problems.
| Feature | Input | Output | Model |
|---|---|---|---|
| Main direction | Parent → Child | Child → Parent | Parent ↔ Child |
| Receives data | Yes | No | Yes |
| Emits changes | No | Yes | Yes |
| Signal-based modern API | input() | output() API | model() |
| Typical use | Configuration/data | User action/event | Editable shared value |
| Example | Product data | Product selected | Quantity |
A useful mental model is:
input() = Give the child something
output() = Child tells parent something happened
model() = Parent and child coordinate one editable value
A common architectural mistake is passing data through many unrelated component levels.
For example:
App
↓
Layout
↓
Page
↓
Container
↓
Toolbar
↓
UserMenu
If every component accepts:
user = input<User>();
only so that UserMenu eventually receives it, the intermediate components become unnecessarily coupled to that data.
For cross-cutting state such as:
a service or another appropriate state-management approach may provide a cleaner architecture.
Suppose a parent passes:
product = {
name: 'Laptop',
price: 50000
};
to a child.
The child receives:
product = input.required<Product>();
Because objects are reference values in JavaScript, careless mutation can make data flow difficult to understand.
Instead of treating every received object as freely mutable, establish clear ownership.
A useful design principle is:
Parent owns state
↓
Child receives state
↓
Child reports requested changes
↓
Owner updates state
This makes state changes easier to trace.
Consider:
Component D
↑
Component C
↑
Component B
↑
Component A
If an event must travel through several components:
D emits
C catches and emits
B catches and emits
A finally handles
the architecture may become difficult to maintain.
This can indicate that the state or action belongs in a shared service or a higher-level state-management mechanism.
Inputs and outputs remain excellent tools, but they should reflect meaningful component relationships rather than become a manual event-routing system.
A reusable component with twenty inputs and fifteen outputs can become difficult to understand.
For example:
title = input('');
subtitle = input('');
color = input('');
size = input('');
width = input('');
height = input('');
border = input('');
font = input('');
padding = input('');
margin = input('');
Sometimes this indicates that the component abstraction is too broad.
A good component API should expose what consumers genuinely need while keeping internal implementation details private.
Ask:
Does the parent really need to control this?
If the answer is no, keep it inside the component.
Component communication should carry clearly defined data.
Instead of:
selected = output<any>();
prefer:
selected = output<Product>();
or:
selected = output<number>();
depending on what the parent actually needs.
Strong typing improves:
The type itself also documents what information the event provides.
Suppose the parent only needs a product ID.
Instead of:
deleted = output<any>();
or sending an unnecessarily large object, use:
deleted = output<number>();
Then:
this.deleted.emit(product.id);
Communication contracts become easier to understand when the payload has a clear purpose.
Consider a reusable product card.
Its responsibility might be:
Display product
Receive interaction
Emit selection
It should not automatically become responsible for:
Updating the database
Managing global cart state
Showing application notifications
Refreshing unrelated components
Handling authentication
Those responsibilities may belong to services, containers, or other architectural layers.
Component communication works best when responsibilities are clearly separated.
Incorrect concept:
Parent needs to provide product
→ use output
Correct:
Parent → Child
→ input
An input receives data.
It does not represent a child-generated event.
Use:
saved = output<void>();
for an event.
If the child emits:
selected.emit(product);
the parent receives that value through:
(productSelected)="handleProduct($event)"
Without $event, the emitted payload is not being passed to the handler.
With:
name = input('');
read:
this.name()
and in a template:
{{ name() }}
because the signal value is obtained by calling it.
An ordinary signal input represents a value supplied by the parent.
When the child itself must update the bound value and communicate that update, consider whether a model input is the appropriate abstraction.
Two-way communication is not automatically better than one-way communication.
Use:
input()
when the child only needs to consume the value.
Use:
model()
when the component genuinely represents an editable value that should propagate changes back to its consumer.
Repeatedly forwarding data through components that do not use it is sometimes called prop drilling.
If several unrelated components require the same state, reconsider where that state should live.
A shared service may be a better owner.
Imagine a dashboard containing:
DashboardComponent
│
├── FilterComponent
├── ProductListComponent
│ └── ProductCardComponent
└── CartSummaryComponent
A sensible communication design could be:
initialCategory = input<string>();
filterChanged = output<Filter>();
product = input.required<Product>();
selected = output<Product>();
ProductCard
↓
CartService
↑
CartSummary
quantity = model(1);
used as:
<app-quantity
[(quantity)]="item.quantity">
</app-quantity>
This design uses each Angular feature where it naturally fits instead of forcing all communication through one mechanism.
When designing communication between Angular components, follow these principles:
model() for genuine editable component values.PARENT TO CHILD
Parent State
↓
input()
↓
Child Component
CHILD TO PARENT
Child Action
↓
output()
↓
Parent Handler
TWO-WAY COMPONENT VALUE
Parent State
↓
model()
↑
Child Update
SHARED COMMUNICATION
Component A
↓
Shared Service
↑
Component B
SIGNAL-BASED SHARED STATE
Component A
↓
Service Signal
↓
Computed State
↓
Component B
After studying Angular component communication, you should be able to:
input().input.required().$event.output().model().[()].computed().The most important concept is not memorizing individual APIs. It is understanding state ownership and communication direction. Once the direction is clear, the appropriate Angular mechanism usually becomes straightforward:
Parent → Child = input()
Child → Parent = output()
Parent ↔ Child = model()
Distant Components = shared service
Reactive Shared State = service + signals