Change Detection

Change detection is the process by which AngularDart checks to see if your component's data has changed, and if so, updates the DOM to reflect those changes.

How Change Detection Works

AngularDart runs change detection in response to:

  • User events (clicks, input, etc.)
  • HTTP responses
  • Timers (setTimeout, setInterval)
  • Observable events (streams)

Change Detection Strategies

Default Strategy

The default strategy checks every component on every event:

@Component(
  selector: 'my-component',
  template: '{{ data.value }}',
  // Default strategy (implicit)
)
class MyComponent {
  Data data = Data();
}

OnPush Strategy

The OnPush strategy only checks when:

  • An @Input() reference changes
  • An event originates from the component or its children
  • An observable emits (with async pipe)
  • Change detection is explicitly triggered
@Component(
  selector: 'user-card',
  template: '{{ user.name }}',
  changeDetection: ChangeDetectionStrategy.OnPush,
)
class UserCardComponent {
  @Input()
  User? user;
}

Important: With OnPush, AngularDart only checks if the reference to user changes, not if properties inside user change.

When OnPush Triggers

Input reference changes

// This triggers change detection
user = User(name: 'Alice');

// This does NOT trigger change detection with OnPush
user.name = 'Bob';

Events from the component

<!-- Clicking this button triggers change detection -->
<button (click)="onClick()">Click me</button>

Async pipe emissions

<!-- Stream emissions trigger change detection -->
<p>{{ data$ | async }}</p>

Manually Triggering Change Detection

Use ChangeDetectorRef to manually control change detection:

@Component(
  selector: 'my-component',
  template: '{{ count }}',
  changeDetection: ChangeDetectionStrategy.OnPush,
)
class MyComponent {
  final ChangeDetectorRef _cd;
  int count = 0;

  MyComponent(this._cd);

  void updateFromExternalSource() {
    // External update (e.g., from a service)
    count++;
    // Manually trigger change detection
    _cd.markForCheck();
  }
}

markForCheck()

Marks the component and its ancestors for check:

_cd.markForCheck();

detectChanges()

Immediately runs change detection on the component and its children:

_cd.detectChanges();

detach() / reattach()

Detach from the change detection tree:

@Component(
  selector: 'heavy-component',
  template: '...',
)
class HeavyComponent {
  final ChangeDetectorRef _cd;

  HeavyComponent(this._cd);

  void pauseUpdates() {
    _cd.detach();
  }

  void resumeUpdates() {
    _cd.reattach();
  }
}

Performance Optimization

Use OnPush for leaf components

@Component(
  selector: 'user-avatar',
  template: '<img [src]="user.avatarUrl">',
  changeDetection: ChangeDetectionStrategy.OnPush,
)
class UserAvatarComponent {
  @Input()
  User? user;
}

Immutable data

Use immutable data structures to make reference changes clear:

class User {
  final String name;
  final String email;

  const User({required this.name, required this.email});

  User copyWith({String? name, String? email}) {
    return User(
      name: name ?? this.name,
      email: email ?? this.email,
    );
  }
}

// Update creates a new reference
user = user.copyWith(name: 'New Name');

TrackBy with ngFor

Use trackBy to avoid re-rendering unchanged items:

<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
dynamic trackById(int index, Item item) => item.id;

Avoid expensive computations in templates

// Bad: computed on every change detection
@Component(
  template: '{{ items.where((i) => i.isActive).length }}',
)

// Good: pre-compute and store
@Component(
  template: '{{ activeItemCount }}',
)
class MyComponent {
  List<Item> items = [];
  int get activeItemCount => items.where((i) => i.isActive).length;
}

Debugging Change Detection

Enable development mode

void main() {
  enableDevMode();
  runApp(AppComponentNgFactory);
}

This throws an error if change detection modifies bindings (ExpressionChangedAfterCheckedError).

Log change detection cycles

@Component(
  selector: 'debug-component',
  template: '...',
)
class DebugComponent implements DoCheck {
  int _checkCount = 0;

  @override
  void ngDoCheck() {
    _checkCount++;
    print('Change detection cycle: $_checkCount');
  }
}

Best Practices

  • Use OnPush for components that don't mutate their inputs
  • Use immutable data structures
  • Use trackBy with ngFor
  • Avoid expensive computations in templates
  • Use markForCheck() for external updates with OnPush
  • Enable dev mode during development