Change detection: When using setTimeout() is not your best option

If you have been working with Angular, then the chances are pretty high that you have run into the ExpressionChangedAfterItHasBeenCheckedError error. Well, I did again last week and what follows is an outline of what I learned while iterating through my ‘solutions’ to reach the correct fix.

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. 

Parent and child components with shared service

There are a few different scenarios that cause this error, but I am only going to focus on the one I faced. I ran into the error with the following setup. We have a parent component that contains a configurable filter component. Both the parent and filter component use a shared filter service.

Our parent component subscribes to the filterValues$ observable from the filter service to receive the latest filter changes.

@Component({...})
export class ParentComponent {
  
  constructor(private filterService: FilterPanelService) {
    // Subscribe to changes of the filter values
    this.filterValues$ = this.filterService.filterValues$;
  }
}

The filter component dynamically creates its inputs based on the config provided by the parent. It is also responsible for initializing the filter service for the given config.

@Component({...})
export class FilterComponent implements OnInit {
  @Input()
  configs: FilterConfig[];

  ngOnInit() {
    // Initialise the filter service based on filter configs
    this.filterService.init(this.configs);
    // Setup filter inputs for configs
  }
}

The filter service provides the initial values for the given config and exposes an observable of the current filter state.

@Injectable()
export class FilterService<T> {
  private filterValueSubject = new Subject<T>();
  public filterValues$: Observable<T>;
  
  constructor(){
    // Expose changes as an observable
    this.filterValues$ = this.filterValueSubject.asObservable();
  }

  init(configs: FilterConfig[]) {
    // Get initial values and push these out
    const initValues = this.getInitialValues(configs);    
    this.filterValueSubject.next(val);
  }
}

Let me try and explain why this setup runs into issues with Angular’s change detection. After the parent component is set up, Angular takes a snapshot of its current state. It then moves down the component tree to setup the filter component.

During the set up of the filter component, we initialize the filter service which causes an update to be sent back to the parent component via its subscription to filterValues$. This means that in dev mode, when the second change detection cycle is run, the snapshotted state of the parent component does not match its current value. This difference is reported to us with the “expression changed after it was checked error”.

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError

Why is this a problem?

Why is Angular telling us that this is a problem? Well, to make Angular’s change detection more efficient, it expects a unidirectional data flow. What we have created is a feedback loop or bidirectional data flow. It means that when a child component updates the bindings of a parent component, that change is not going to be reflected in the DOM until another change detection cycle is run. This will manifest to the user as a stale/invalid state.

Feedback loop via service observable

I would highly recommend taking the time to read the following two articles by Maxim Koretskyi. The first fully explains unidirectional data flow, and the second goes into great detail about the expression changed error. These two articles helped me understand this topic so much better!

Incorrect: Just use the OnPush strategy…

One way of stopping the exception is to update the ChangeDetectionStrategy to OnPush.

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
}

This stops the exception being thrown, but it does not fix the bug that we have in our code that Angular is trying to warn us about!

Below our filter component, we have a button that is conditionally disabled based on the filter state. At the time our parent component is set up, the filter values have not been initialized and so the button is disabled. During the setup of the filter component, the values are initialized meaning the button should be enabled, but it is still disabled. Triggering any event in our view, i.e., focus/blur, suddenly causes the button to become enabled. This happens as change detection has been triggered by the DOM events and so our component then correctly reflects its state.

So, don’t just change to OnPush to silence the development environment exception. You very likely will still have a bug when running in production!

Okayish: Use a setTimeout…

Another very popular solution is to make the problematic code async. This is done by using a setTimeout or adding delay(0) to our pipe in RxJs. In our case, we could add a delay(0) to the filterValues$ observable to make these updates happen asynchronously.

    // BEFORE: Synchronous updates with error
    //this.filterValues$ = this.filterValueSubject.asObservable();

    // AFTER: Asynchronous updates working
    this.filterValues$ = this.filterValueSubject.asObservable()
                                                .pipe(delay(0));

This works because change detection runs synchronously so the values in the component are still the same after the filter service has been set up. Once this is completed, our asynchronous update kicks off, triggering a second change detection cycle that enables our component to reflect the correct data state.

While this approach resolves the exception and fixes our display bug, we have cluttered our code and degraded the performance of our app. So, while this is a popular solution, it clearly is not optimal.

Can we do any better?

Best: Respect unidirectional data flow

Instead of working around Angular’s requirement for unidirectional data flow, let’s update our data flow to respect it. For me, this took a bit more thought to get right, but the result is definitely worth it.

As a reminder, our issue is caused by the feedback loop of the filter service being initialized by the filter component, which then causes an update in the parent component.

The light bulb moment came when I realized that we could get the parent component to initialize the filter service as part of its setup. This means that the parent has the final filter values before the child filter component is setup. This way, when our filter component is set up, the filter service is already primed and the filter component can just get its initial values. We now have unidirectional data flow and, unsurprisingly, our errors all disappear!


@Component({})
export class ParentComponent {
  private configs = [...];

  constructor(private filterService: FilterPanelService) {
    this.filterValues$ = this.filterService.filterValues$;
    
    // Initialise filter service in parent
    this.filterService.init(this.configs);
  }
}

@Component({})
export class FilterPanelComponent implements OnInit {
  @Input()
  configs: FilterConfig[];

  ngOnInit() { 
      //No need to initialise the filter service here

      //Setup filter inputs based on configs
  }
}

//No changes required
export class FilterService<T> {}

No feedback loop anymore

Success! We have no exceptions, no bugs, no work around code, and no performance degradation. We have also reduced the risk of future bugs as we no longer have a mysterious delay(0) in our code. I bet in a year’s time, someone (possibly me) will think, “what’s the point of that?” and delete it creating a bug.

Moral of the story? Think about your data flows and don’t just reach for setTimeout every time you run into ExpressionChangedAfterItHasBeenError error.

Free Resources