Angular: Stop Overusing ChangeDetectorRef
Source: Dev.to
In Angular development, we often run into change detection issues, for example when some fields are not updated properly in the template. At that point, we need to investigate where the problem is, and since AI is now part of our day‑to‑day work, we often use it to help identify the root cause.
However, I have observed many times that Copilot suggests using detectChanges() or markForCheck() as a quick fix for various problems. In most cases, these methods do not solve the root cause. The actual issue is usually an incorrect data flow between components or poor architecture. These methods should be used only in specific situations — definitely not as a workaround for misunderstanding or overusing the OnPush change detection strategy.
Understanding Change Detection Strategies
| Method | Description |
|---|---|
| ChangeDetectorRef.detectChanges() | Runs change detection immediately for the current component and its children. |
| ChangeDetectorRef.markForCheck() | Explicitly marks the view as changed so that it can be checked again in the next detection cycle. |
ChangeDetectionStrategy.Default
Angular checks every component on every change‑detection cycle.
ChangeDetectionStrategy.OnPush
Angular only checks a component when:
- An
@Inputreference changes - An async pipe receives a new value
- An event is triggered from the template
detectChanges()ormarkForCheck()is called
NOTE: When talking about change detection, it is also important to mention
zone.js. Angular’szone.jslibrary automatically triggers change detection after async operations likesetTimeout, HTTP requests, and event handlers. This means manualdetectChanges()is rarely needed.
Common Anti‑patterns
detectChanges()
@Component({
selector: 'app-user',
template: '{{ user?.name }}',
changeDetection: ChangeDetectionStrategy.Default
})
export class UserComponent {
@Input() user: User | undefined;
constructor(private cdr: ChangeDetectorRef) {}
// ❌ WRONG: Mutating @Input and calling detectChanges()
updateUser(user: User) {
this.user = user;
this.cdr.detectChanges(); // Doesn't fix the architecture problem
}
}
Problems
- The
usershould be updated via@Input()from the parent component, not through an internalupdateUsermethod. - Calling
detectChanges()is unnecessary because Angular should detect the change automatically.
Both issues indicate a problem with the component’s architecture.
@Component({
selector: 'app-data',
template: '{{ data }}',
changeDetection: ChangeDetectionStrategy.Default
})
export class DataComponent {
data = '';
constructor(
private service: DataService,
private cdr: ChangeDetectorRef
) {}
// ❌ WRONG: Using detectChanges() instead of fixing data flow
loadData() {
this.service.getData().subscribe(result => {
this.data = result;
this.cdr.detectChanges(); // Workaround for bad architecture
});
}
}
Problem – detectChanges() is used inside a subscription. Angular already runs change detection after the observable emits, so this is a sign that the data flow or architecture is flawed.
markForCheck()
@Component({
selector: 'app-user-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '{{ user.name }}'
})
export class UserListComponent {
@Input() users: User[] = [];
constructor(private cdr: ChangeDetectorRef) {}
// ❌ WRONG: Mutating @Input internally and then calling markForCheck()
addUser(user: User) {
this.users.push(user);
this.cdr.markForCheck();
}
}
Problem – With OnPush, inputs should be treated as immutable. The parent component must create a new array reference (this.users = [...this.users, user]) instead of mutating the existing one.
// ❌ WRONG: OnPush component should receive data via inputs
@Component({
selector: 'app-user',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '{{ user.name }}'
})
export class UserComponent {
user: User = { name: 'John', age: 30 };
constructor(
private service: UserService,
private cdr: ChangeDetectorRef
) {
this.service.userNameChanged$.subscribe(userName => {
this.user.name = userName;
this.cdr.markForCheck();
});
}
}
Problem – The component mutates an internal user object and forces an update with markForCheck(). A better approach is to expose user as an @Input() and let the parent supply a new object reference whenever the name changes.
When Is markForCheck() Acceptable?
OnPush works best when components are driven by immutable inputs and unidirectional data flow. There are valid cases for markForCheck() (e.g., when a service emits a value that isn’t bound via the async pipe), but if you find yourself calling it frequently, reconsider whether OnPush is the right choice for that component.
Conclusion
ChangeDetectorRef is a powerful tool, but relying on it frequently signals architectural problems. Both markForCheck() and detectChanges() often mask underlying issues rather than solving them.
Instead, focus on proper data flow:
- Use immutable objects and replace inputs with new references.
- Leverage the
asyncpipe for observable streams. - Keep components small and focused, letting parents manage state.
When the data flow is correct, Angular’s built‑in change detection (with or without OnPush) will keep the UI in sync without the need for manual interventions.
For observables, maintain immutability with OnPush, and ensure components receive new references when data changes. If you find yourself calling these methods regularly, it’s time to step back and redesign your data flow.
Advice: Treat AI suggestions involving
ChangeDetectorRefas a starting point, not a final answer. WhileChangeDetectorRefcan, in specific cases, solve real problems, you should always pause and double‑check whether the issue actually lies in your data flow or component design.