Angular: Stop Overusing ChangeDetectorRef

Published: (January 16, 2026 at 04:18 AM EST)
4 min read
Source: Dev.to

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

MethodDescription
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 @Input reference changes
  • An async pipe receives a new value
  • An event is triggered from the template
  • detectChanges() or markForCheck() is called

NOTE: When talking about change detection, it is also important to mention zone.js. Angular’s zone.js library automatically triggers change detection after async operations like setTimeout, HTTP requests, and event handlers. This means manual detectChanges() 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

  1. The user should be updated via @Input() from the parent component, not through an internal updateUser method.
  2. 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
    });
  }
}

ProblemdetectChanges() 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 async pipe 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 ChangeDetectorRef as a starting point, not a final answer. While ChangeDetectorRef can, 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.

Back to Blog

Related posts

Read more »

Anguar Tips #4

Introduction Some tips for working with Angular – from a frontend developer Part 4. These tips assume you already have experience with Angular, so we won’t div...