Key Takeaways
ngOnChangeshandles Angular-delivered input changes, including initial bound values beforengOnInit.SimpleChangescontains aSimpleChangefor each input Angular reports as changed in that check.- A local assignment to an input property does not itself invoke
ngOnChanges. - Object inputs are compared by reference, not by deeply inspecting their nested properties.
Angular ngOnChanges is a lifecycle hook that runs when Angular initializes or updates a component's inputs. Its SimpleChanges argument lets you inspect previous and current input values and identify the first change. It works with both decorator-based and signal-based inputs.
When Angular ngOnChanges Runs
The important distinction is between an input update performed by Angular and an ordinary property assignment. The hook participates in Angular's input-binding and change-detection process; it isn't a general-purpose observer attached to your class fields.
For a component with a bound input, the relevant lifecycle sequence is:
Initial check:
Angular assigns bound inputs
-> ngOnChanges(changes)
-> ngOnInit()
-> Angular checks the child's template
Later check with changed bound inputs:
Angular updates inputs
-> ngOnChanges(changes)
-> Angular checks the child's templateThe first ngOnChanges runs before ngOnInit, and subsequent calls happen before the component's own template is checked. A field initializer alone, without an Angular input assignment, doesn't create an input-change notification.
Angular ngOnChanges Example: Parent to Child
This example uses standalone components with explicit imports and bootstrapping. It keeps the supported @Input decorator so you can demonstrate a local assignment; Angular recommends signal-based inputs for new projects.
With Angular CLI installed, create a browser application using the documented ng new options, answering any remaining prompts:
ng new changes-demo --standalone --routing=false --ssr=false --style=css
cd changes-demoChild Component
Create src/app/child.component.ts:
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import type { OnChanges, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Child value: {{ parentData }}</p>
<button type="button" (click)="changeFromChild()">
Change from child
</button>
`,
})
export class ChildComponent implements OnChanges, OnInit {
@Input() parentData = 0;
changeFromChild(): void {
// Demonstration only: a local write is not an Angular input update.
this.parentData -= 1;
}
ngOnChanges(changes: SimpleChanges): void {
console.log('ngOnChanges', changes);
const change = changes['parentData'];
if (change) {
console.log({
previousValue: change.previousValue,
currentValue: change.currentValue,
firstChange: change.firstChange,
isFirstChange: change.isFirstChange(),
});
}
}
ngOnInit(): void {
console.log('ngOnInit', this.parentData);
}
}Parent Component
Create src/app/parent.component.ts:
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [ChildComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Parent value: {{ data }}</p>
<button type="button" (click)="changeFromParent()">
Change from parent
</button>
<app-child [parentData]="data"></app-child>
`,
})
export class ParentComponent {
data = 0;
changeFromParent(): void {
this.data += 1;
}
}The parent imports ChildComponent so Angular can resolve app-child in its template. The [parentData]="data" binding passes the parent's number into the child.
Bootstrap the Application
Replace src/main.ts with the following. bootstrapApplication renders the standalone parent as the application's root component; ensure src/index.html contains its host element, <app-root></app-root>.
import { bootstrapApplication } from '@angular/platform-browser';
import { ParentComponent } from './app/parent.component';
bootstrapApplication(ParentComponent).catch((error: unknown) => {
console.error('Application bootstrap failed', error);
});Run ng serve and open the address printed by the CLI. The command builds and serves the application, rebuilding when files change.
Observe Parent Updates and Local Writes
- On startup, Angular delivers
0to the child. You'll seengOnChangesbeforengOnInit. - Click Change from parent. The parent's value becomes
1, Angular updates the child's input, and the hook runs again. - Click Change from child. The child's value becomes
0, while the parent remains1. That local assignment doesn't invoke the hook. - Click Change from parent again. The parent becomes
2, Angular delivers that value, and the hook runs.
These results follow from Angular tracking binding values and input-change history separately from arbitrary local writes. In the final step, previousValue is 1, the previous Angular-delivered input value—not the child's locally edited 0.
Reading Angular SimpleChanges
SimpleChanges maps declared input property names to SimpleChange objects. For the example, the initial call contains the following values; this is an illustrative console representation, not JSON:
{
parentData: SimpleChange {
previousValue: undefined,
currentValue: 0,
firstChange: true
}
}After the first parent click, without any intervening child edits:
{
parentData: SimpleChange {
previousValue: 0,
currentValue: 1,
firstChange: false
}
}The initial previousValue is undefined even though the class field starts at 0: there was no previous Angular-delivered input value. isFirstChange() returns the first-change flag, so it returns true for the initial assignment shown above and false for the subsequent update.
For multiple inputs, inspect whichever entries matter to your calculation. Only inputs reported as changed appear in that call's object, so guard an entry before reading it. SimpleChanges isn't a complete snapshot of every input or a record of every nested mutation.
Input Aliases Do Not Change the SimpleChanges Key
If you replace the input declaration with this fragment, the parent binds with [count]="data", but the hook still reads changes['parentData']. Angular uses the TypeScript property name, not its template alias, as the key.
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: '',
})
export class ChildComponent {
// Fragment inside ChildComponent, replacing the original declaration:
@Input({ alias: 'count' }) parentData = 0;
}Object Inputs: Reference Changes vs. Nested Mutation
For an object input, changing a nested field doesn't change the object reference. Given a child input declared as @Input() profile = { name: '' }; and a parent binding of [profile]="profile", compare these parent-side fragments:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: '',
})
export class ParentComponent {
// Fragment inside the parent component:
profile = { name: 'Ada' };
mutateProfile(): void {
// Same object reference: no profile input-change notification.
this.profile.name = 'Grace';
}
replaceProfile(): void {
// New object reference: Angular can detect a changed bound input.
this.profile = { ...this.profile, name: 'Grace' };
}
}Call these methods from template-bound button events to compare the behavior. The mutation doesn't produce a profile entry in SimpleChanges; replacement produces one when Angular processes the changed binding.
OnPush doesn't turn this hook into a deep watcher. A template may show a mutated nested value if another event causes that child to be checked, but that still doesn't mean the object input changed by reference. Also, previousValue holds an object reference, not a historical deep clone.
ngOnChanges vs ngOnInit
| Question | ngOnChanges | ngOnInit |
|---|---|---|
| When does it run? | When Angular delivers changed input values, including initialization. | Once after Angular initializes the inputs. |
| Which runs first? | The initial call precedes ngOnInit. | Runs after the initial ngOnChanges, when one occurs. |
| Does it receive change records? | Yes: SimpleChanges. | No arguments. |
| Best fit | Logic that must respond to input updates. | One-time initialization that doesn't need to repeat for input updates. |
You don't need both hooks in every component. Input-dependent initialization can live in ngOnChanges because it already handles initial bound values; reserve Angular ngOnInit for work that should happen once.
When to Use ngOnChanges—and When Not To
Use the hook when you need previous-versus-current values or a single place to coordinate updates across multiple inputs. For example, a component can update a stored total when either price or quantity changes:
import { Component, Input } from '@angular/core';
import type { OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-total',
standalone: true,
template: '',
})
export class TotalComponent implements OnChanges {
// Fragment inside a component implementing OnChanges.
// Reuse the Input and SimpleChanges imports from the child example.
@Input() unitPrice = 0;
@Input() quantity = 1;
total = 0;
ngOnChanges(changes: SimpleChanges): void {
if (changes['unitPrice'] || changes['quantity']) {
this.total = this.unitPrice * this.quantity;
}
}
}Don't introduce stored state for every calculation. For cheap derived values, a getter is often simpler. With signal inputs, computed expresses the dependency directly and caches the result until its signal dependencies change.
import {
ChangeDetectionStrategy,
Component,
computed,
input,
} from '@angular/core';
@Component({
selector: 'app-total',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>Total: {{ total() }}</p>`,
})
export class TotalComponent {
readonly unitPrice = input(0);
readonly quantity = input(1);
readonly total = computed(() => this.unitPrice() * this.quantity());
}This alternative doesn't need ngOnChanges for its derived total. Signal inputs still participate in the hook when you implement it; using signals doesn't disable lifecycle change records.
Common Mistakes
- Expecting local writes to behave like input bindings. Keep the distinction visible in your component design and tests.
- Reading an alias as the change-record key. Use the declared TypeScript property name.
- Expecting deep mutation tracking. Prefer replacing changed objects rather than adding expensive deep comparisons to every check.
- Moving the problem into another hook. Angular ngDoCheck runs frequently; it isn't a free substitute for a clear input-update contract.
FAQ
Does ngOnChanges run for the initial input value?
Yes. When Angular assigns an initial bound input value, ngOnChanges runs before ngOnInit. For the initial assignment in this example, previousValue is undefined and firstChange is true.
Does ngOnChanges detect changes to nested object properties?
Not when you mutate an object passed as an input while preserving its reference. Replace the object to produce a changed object input, or bind the particular primitive property the child needs.
What is the order of ngOnChanges and ngOnInit?
For initial bound inputs, ngOnChanges runs first, followed by ngOnInit. ngOnInit runs once per component instance; ngOnChanges can run again for later input updates.
How do I read previous and current input values with SimpleChanges?
Look up the input's declared property name, such as changes['parentData'], and read its previousValue and currentValue fields. Check that the entry exists, and use isFirstChange() or firstChange to identify the initial change.
Why is ngOnChanges not called after changing a property inside the child?
A direct assignment such as this.parentData -= 1 is a local property write, not an Angular input update. A later parent binding change can still update that input and invoke ngOnChanges.
Further Reading
- Angular component lifecycle — hook timing, initialization, and execution order.
- OnChanges API — the hook contract and support for both input styles.
- SimpleChanges API — change records keyed by declared property names.
- SimpleChange API — previousValue, currentValue, firstChange, and isFirstChange().
- Angular input properties — decorator inputs, signal inputs, and aliases.
- OnPush and skipping component subtrees — input references and change-detection behavior.
- Angular signals — computed values and dependency tracking.
- Angular component anatomy — component metadata and template imports.
