Technology  /  Angular

๐Ÿ…ฐ๏ธ Angular 15 guides ยท updated 2026

Standalone components, signals, RxJS, and dependency injection โ€” building maintainable frontends with the framework enterprises trust.

Angular Component Communication: Signal Inputs, Outputs, and Shared State

A component alone is easy. Applications are conversations โ€” a parent configuring children, children reporting events upward, siblings needing the same data, a header needing to know what a deeply-nested cart contains. Choosing the right channel for each conversation is most of Angular architecture; choosing wrong is how apps become webs of tangled references nobody can refactor.

This guide covers the modern toolkit โ€” signal input(), output(), model(), view queries โ€” and the decision framework for when parent-child patterns stop being enough.


Inputs: Data Flows Down

The child declares what it accepts; the parent binds values in:

cart-item-row.ts
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-cart-item-row',
template: `
<div class="row" [class.bulk]="isBulk()">
{{ item().name }} ร— {{ item().qty }} โ€” {{ lineTotal() / 100 }}
</div>
`,
})
export class CartItemRow {
item = input.required<CartItem>(); // required: compiler enforces binding
discountPct = input(0); // optional, with default
lineTotal = computed(() =>
item().paisePerUnit * item().qty * (1 - this.discountPct() / 100));
isBulk = computed(() => this.item().qty >= 10);
}
<!-- parent -->
<app-cart-item-row [item]="line" [discountPct]="memberDiscount()" />

The modern details:

Transform functions handle input normalization declaratively: disabled = input(false, { transform: booleanAttribute }) lets callers write bare disabled attributes HTML-style.


Outputs: Events Flow Up

Children donโ€™t call parents โ€” they announce, and parents decide what it means:

// child
export class CartItemRow {
item = input.required<CartItem>();
remove = output<string>(); // event carrying the item id
qtyChange = output<number>();
onRemoveClick() {
this.remove.emit(this.item().id);
}
}
<!-- parent template -->
<app-cart-item-row
[item]="line"
(remove)="cart.removeItem($event)"
(qtyChange)="cart.setQty(line.id, $event)"
/>

output<T>() declares the event; .emit(value) fires it; the parent binds with ( ) and receives the payload as $event โ€” the same syntax as DOM events, deliberately. Design guidance that keeps components reusable:

model(): sanctioned two-way

When a componentโ€™s whole point is editing a value โ€” inputs, sliders, toggles โ€” declare a model:

export class QtyStepper {
qty = model.required<number>(); // input + output, fused
inc() { this.qty.update((q) => q + 1); } // child MAY write a model
dec() { this.qty.update((q) => Math.max(1, q - 1)); }
}
<app-qty-stepper [(qty)]="line.qty" /> <!-- banana box works on YOUR component -->

model() is the two-way sugar formalized: a writable signal that syncs both directions. The restraint rule: models are for value-editing widgets; broad two-way binding of business state re-creates the untraceable data flows one-way design exists to prevent.


View Queries: When the Parent Needs a Handle

Occasionally a parent must address a child imperatively โ€” focus it, open it, measure it:

export class CheckoutPage {
addressForm = viewChild.required(AddressForm); // signal of the child instance
submitBtn = viewChild<ElementRef>('submitRef'); // or a template ref variable
submitAll() {
if (!this.addressForm().validate()) return;
// ...
}
}

viewChild() (and viewChildren()) return signals resolving to child component instances or elements โ€” the modern, timing-safe form of the old @ViewChild decorator with its lifecycle-timing gotchas. Legitimate uses: focus management, imperative APIs on widgets (open a dialog, reset a form), measurement. The smell: using queries to read child state routinely โ€” thatโ€™s data that should flow via outputs or shared services. Queries are for commanding, sparingly; if parent and child converse through queries, the design has inverted.


Beyond Parent-Child: Services as Shared State

Inputs/outputs shine for adjacent components and degrade with distance โ€” passing data through four intermediate components that donโ€™t care about it (โ€œprop drillingโ€) couples everyone to everyone. The Angular answer is a service holding shared state, injected by whoever needs it:

cart-store.ts
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CartStore {
private items = signal<CartItem[]>([]);
readonly count = computed(() => this.items().length);
readonly totalPaise = computed(() =>
this.items().reduce((s, i) => s + i.paisePerUnit * i.qty, 0));
add(item: CartItem) { this.items.update((xs) => [...xs, item]); }
removeItem(id: string) { this.items.update((xs) => xs.filter((x) => x.id !== id)); }
}
// any component, any depth:
export class HeaderBadge {
cart = inject(CartStore);
// template: {{ cart.count() }} โ€” updates automatically, forever
}

The header badge, the cart page, and the product buttons all inject one CartStore instance and share its signals โ€” no drilling, no event relays, updates propagating reactively to every reader. Notice the encapsulation: the writable signal is private; the world gets read-only computeds and named mutation methods โ€” the invariant-guarding discipline at store scale. This service-with-signals pattern is modern Angularโ€™s default state management, covered fully in the signals guide and DI guide.

The decision framework

parent โ†” direct child, data/config โ†’ input()
child โ†’ parent, something happened โ†’ output()
widget edits one value โ†’ model()
parent commands child imperatively โ†’ viewChild (sparingly)
siblings / distant / many readers โ†’ shared service with signals
caller supplies markup, not data โ†’ content projection (ng-content)

The reliable smells: state passed through components that donโ€™t use it (โ†’ service), outputs re-emitted up multiple layers (โ†’ service), and sibling refs handed around so one can call the other (โ†’ always a service).


Worked Refactor: From Prop Drilling to a Store

The decision framework is easiest to absorb watching one design evolve. Version one โ€” a product page where the buy button needs cart access:

ProductPage โ†’ ProductInfo โ†’ PriceBox โ†’ BuyButton
[cart] [cart] [cart] (uses it)

Three components relay a cart input they never read, and each grows an output chain relaying (added) events back up. Every intermediate componentโ€™s API is polluted by its descendantsโ€™ needs โ€” the drilling smell in its purest form. Adding a wishlist button two levels deep elsewhere would double the plumbing.

Version two โ€” the store refactor:

// BuyButton, directly:
export class BuyButton {
private cart = inject(CartStore);
product = input.required<Product>(); // still takes its OWN data as input
add() { this.cart.add(toCartItem(this.product())); }
}

ProductInfo and PriceBox lose the pass-through input and output entirely โ€” their APIs shrink back to what they render. The header badge elsewhere reads cart.count() with zero connection to this subtree. Note what didnโ€™t move: product stays an input, because itโ€™s this componentโ€™s local rendering data with a clear parent owner โ€” the store took only the cross-cutting state.

The judgment line this example draws: inputs/outputs describe a componentโ€™s relationship with its parent; stores describe stateโ€™s relationship with the app. When a piece of dataโ€™s readers and writers stop being ancestors and descendants of each other, it has outgrown the input system โ€” and moving it is a simplification of every component in between, which is how you know the timing was right. (The reverse migration matters too: a store holding state only one subtree uses is global-by-laziness โ€” push it down to a component-provided service or plain inputs.)

Frequently Asked Questions

When do I still need ngOnChanges? Rarely, in signal-era code โ€” reacting to input changes is what computed (derive) and effect (side effects) do naturally. ngOnChanges remains in legacy components with decorator inputs; the lifecycle guide covers the mapping.

Can a child inject its parent component? Yes (inject(CartPage)) โ€” element-hierarchy DI allows it โ€” and it couples the child to that exact parent, destroying reuse. Reserve it for tightly-bound component families (tabs + tab, accordion + panel) where the coupling is the design; otherwise use the patterns above.

How do sibling components communicate without a service? Through their common parent: Aโ€™s output updates parent state, parent binds it into B. Thatโ€™s fine for one or two hops; the moment it feels like relaying, that state has outgrown the parent โ€” service time.

What replaced @Input() set x(...) setter tricks? Deriving with computed(() => transform(this.x())) for derived values, effect for genuine side effects, and transform: on the input for normalization โ€” each intent now has a named tool instead of one overloaded setter idiom.

Do inputs work with OnPush / zoneless? Signal inputs are the foundation both are built on โ€” inputs are signals, so change detection knows exactly which components a change touches. This is a large part of why the migration to signal inputs matters beyond ergonomics.


Reading Legacy Communication Code

The decorator-era equivalents, mapped for the codebases youโ€™ll inherit: @Input() item!: Product (the ! asserting โ€œtrust me, itโ€™ll be boundโ€ โ€” modern input.required made it a real contract); @Input() set qty(v) {...} setter-interception (โ†’ computed/transform); @Output() remove = new EventEmitter<string>() (โ†’ output() โ€” same .emit, same template binding, so mixed codebases feel seamless); @ViewChild('ref') el!: ElementRef with its ngAfterViewInit timing dance (โ†’ viewChild signals, timing-safe); and BehaviorSubject-based shared services with async pipes at every consumer (โ†’ signal stores โ€” the architectural shape is identical, only the reactive primitive changed). Migration schematics (signal-input-migration, output-migration) automate most of the mechanical conversions. The durable insight: the patterns โ€” data down, events up, stores for distance โ€” predate signals and survived them; only the syntax modernized. Reading a 2019 component, youโ€™re reading this guideโ€™s architecture in an older dialect.

A final design lens worth keeping: a componentโ€™s inputs and outputs are its API, and API review standards apply. Would you approve a function with nine parameters, three of them booleans? Then a component with nine inputs deserves the same scrutiny โ€” group related inputs into one object input, or question whether the component is several components. Same for outputs that expose internals ((internalStateChanged)) rather than meaningful events. Components with clean, small APIs get reused; components with sprawling ones get copy-pasted and forked โ€” the codebase tells you which youโ€™ve built.

Self-Check

  1. A UserAvatar component crashes when parents forget [user]. Which API turns that runtime crash into a compile error?
  2. Why should CartItemRow emit (remove) with an id rather than calling cartService.removeItem(...) itself?
  3. [(value)]="query" on your custom SearchBox โ€” what must SearchBox declare for this to work, in both modern and pre-model() forms?
  4. A settings toggle five levels deep must update a header indicator. Walk the two candidate designs and pick.
  5. A parent uses viewChild to read child.selectedItems() every render. Whatโ€™s the smell, and the fix?

(Answers: input.required<User>(). Reusability and single ownership โ€” the row works in any list context when the parent decides meaning; also the row stays service-free and trivially testable. Modern: value = model<string>(); legacy: @Input() value + @Output() valueChange. Input-drilling through five layers vs a shared SettingsStore service โ€” service wins at that distance. Queries-as-data-channel; expose the selection via an output or shared store โ€” queries are for commands.)

Next: Lifecycle Hooks โ€” what Angular calls when, effect and afterNextRender in the signal era, and the cleanup discipline that prevents leaks.