The component contract
How data gets in, how events get out, and why `ngOnChanges` is no longer part of the answer.
Inputs are signals
An input used to be a decorated field that the framework wrote into at the right moment, which is why you needed ngOnChanges and a SimpleChanges object to react to it. An input is now a signal you read:
import { Component, input } from '@angular/core';
@Component({ selector: 'app-badge', template: `<span>{{ label() }}</span>` })
export class Badge {
readonly label = input.required<string>(); // must be provided by the parent
readonly tone = input<'info' | 'warn'>('info'); // optional, with a default
}input<T>()gives youInputSignal<T | undefined>unless you pass a default.input.required<T>()gives youInputSignal<T>and fails at compile time if the parent forgets it. That is almost always what you want: a required input should be a build error, not a runtime surprise.- The value is read with
label(), so an input behaves like any other signal in acomputed, aneffector a template.
2022 — decorator, and ngOnChanges to react
2026 — signal input, and computed
The second column is not just shorter. The derived value is correct from the very first render, because a computed reads the input when it is read — there is no window between "input set" and "lifecycle hook ran" in which the template sees a stale value.
Outputs and two-way binding
An output is still an event emitter, now with its own function:
import { Component, output, model } from '@angular/core';
@Component({ selector: 'app-pager', template: `...` })
export class Pager {
readonly changed = output<number>(); // parent binds (changed)="..."
readonly page = model(1); // parent binds [(page)]="currentPage"
next() {
this.page.update((page) => page + 1); // writes the model signal...
this.changed.emit(this.page()); // ...and tells whoever cares
}
}model() is a writable signal owned by the child, bound two-way by the parent. Before v17 you had @Input() value plus @Output() valueChange and a naming convention the compiler could not check; a typo in either half failed silently. Now it is one declaration that both directions understand:
<app-pager [(page)]="currentPage" (changed)="onPage($event)" />Use output() for "something happened" and model() for "a value the parent and child both own". Mixing them up is what produces the classic two-way binding loop.
input.required, model, output
One child component with a required input, a two-way model and an output. Press the child button and the parent sees the value change; press the parent button and the change flows back down.
parent: count = 0
Quantity = 0
(bumped) events: no events yet
Queries are signals too
@ViewChild came with AfterViewInit, a non-null assertion, and a setter if you wanted to react to it. A signal query is a signal:
readonly inputEl = viewChild.required<ElementRef<HTMLInputElement>>('search');
readonly panels = viewChildren(Panel);
readonly projectedHeader = contentChild(Header);
ngOnInit() {
// safe: the query resolves before ngOnInit for static children
this.inputEl().nativeElement.focus();
}viewChild reads it once, viewChildren gives you a signal of the array, and contentChild looks into projected content. Pair them with afterNextRender or afterRenderEffect when the work needs the real DOM.
What is left of the lifecycle
The hooks you used are still there, and they still mean what they meant. What has gone is the need for two of them:
ngOnInit— still the right place for one-off setup that needs inputs.ngOnDestroy— still right for manual teardown; with signals, most teardown simply disappears.ngOnChanges— no longer needed for reacting to inputs. Usecomputed(); useeffect()only if the reaction is a side effect rather than a value.ngAfterViewInit— mostly replaced byafterNextRender, which does not run during prerendering and does not need a hook on every component that touches the DOM.
export class Chart {
constructor() {
afterNextRender(() => {
this.renderInto(this.canvas().nativeElement); // browser only
});
}
}For DOM reading that must produce a value, afterRenderEffect is the v22 tool: it is an effect that runs after rendering, and it returns a signal instead of writing into a field.
Two traps that the decorator era hid and the signal era does not. First, never derive state in ngOnChanges — it runs after the template has already been checked once, which is where the "one frame behind" bugs came from; use computed. Second, do not read an input in the constructor and assume it is set: with signal inputs the value is there when it is read in a computed or a hook, not necessarily when the field initialiser runs.
Why the contract matters
Everything in this page is one idea: the data a component receives and sends is observable from its own code, with no lifecycle bookkeeping in between. Once inputs and queries are signals, a component can compute everything it needs in the same declarative style you use for local state — which is the subject of Signals, and the reason templates can be plain markup again in Templates & control flow.