Ngx-toastr: Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked

Created on 9 Aug 2017  路  12Comments  路  Source: scttcper/ngx-toastr

Receiving the following error:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'toast-success toast'. It seems like the view has been created after its parent and its children have been dirty checked. Has it been created in a change detection hook ?
at viewDebugError (core.es5.js:8420)
at expressionChangedAfterItHasBeenCheckedError (core.es5.js:8398)
at checkBindingNoChanges (core.es5.js:8562)
at checkNoChangesNodeInline (core.es5.js:12423)
at checkNoChangesNode (core.es5.js:12397)
at debugCheckNoChangesNode (core.es5.js:13174)
at debugCheckRenderNodeFn (core.es5.js:13114)
at Object.eval [as updateRenderer] (Toast_Host.html:1)
at Object.debugUpdateRenderer [as updateRenderer] (core.es5.js:13096)
at checkNoChangesView (core.es5.js:12219)

This is caused by the following code:

import { ToastrService } from 'ngx-toastr';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app';

  constructor(private toastr: ToastrService) { }

  public ngOnInit(): void {
    this.toastr.success("testing", "testing");
  }
}

This only seems to happen when a toast is created automatically on load. Most likely because not everything is loaded yet?

Doesn't happen when everything is loaded (ex: on button press)

Most helpful comment

call it inside a setTimeout

ngOnInit() {
    setTimeout(() => this.toastr.success('sup'))
}

All 12 comments

I haven't been able to track down exactly where the issue is happening but modifying the _buildNotification() method by surrounding the actual toast creation with a setTimeout() "fixes" the issue.

Something like this:

private _buildNotification(
    toastType: string,
    message: string,
    title: string,
    config: GlobalConfig,
  ): ActiveToast | null {
    // max opened and auto dismiss = true
    if (this.toastrConfig.preventDuplicates && this.isDuplicate(message)) {
      return null;
    }
    this.previousToastMessage = message;
    let keepInactive = false;
    if (this.toastrConfig.maxOpened && this.currentlyActive >= this.toastrConfig.maxOpened) {
      keepInactive = true;
      if (this.toastrConfig.autoDismiss) {
        this.clear(this.toasts[this.toasts.length - 1].toastId);
      }
    }
    this.index = this.index + 1;
    const ins: ActiveToast = {
      toastId: this.index,
      message,
      toastRef: null,
      onShown: null,
      onHidden: null,
      onTap: null,
      onAction: null,
    };
    setTimeout(() => {
      const overlayRef = this.overlay.create(config.positionClass, this.overlayContainer);
      let sanitizedMessage = message;
      if (message && config.enableHtml) {
        sanitizedMessage = this.sanitizer.sanitize(SecurityContext.HTML, message);
      }
      const toastRef = new ToastRef(overlayRef);
      const toastPackage = new ToastPackage(
        this.index,
        config,
        sanitizedMessage,
        title,
        toastType,
        toastRef,
      );
      ins.toastRef = toastRef;
      ins.onShown = toastRef.afterActivate();
      ins.onHidden = toastRef.afterActivate();
      ins.onTap = toastPackage.onTap();
      ins.onAction = toastPackage.onAction();
      const toastInjector = new ToastInjector(toastPackage, this._injector);
      const component = new ComponentPortal(config.toastComponent, toastInjector);
      ins.portal = overlayRef.attach(component, this.toastrConfig.newestOnTop);
      if (!keepInactive) {
        setTimeout(() => {
          ins.toastRef.activate();
          this.currentlyActive = this.currentlyActive + 1;
        });
      }
    });
    this.toasts.push(ins);
    return ins;
  }

I don't think I like the solution though. What do you think @scttcper, any ideas?

Nice. No wonder I couldn't find the issue. Guess the last time I tried doing a test like this was pre Angular 2.3.x

I don't know what a legitimate use case would be to show a toast in a life cycle event would be. It's just use it for quick tests to make sure I did everything correctly.

i do think its what most people try first when adding the toastr. I sure did. Annoying it doesn't work without settimeout.

Is there a way to solve this without modifying the source code of the npm package?

call it inside a setTimeout

ngOnInit() {
    setTimeout(() => this.toastr.success('sup'))
}

I just got this error today - with a legitimate use case, which is somewhat complicated, but here goes:

Users signs up and the api sends a confirmation link to their email. Clicking the email hits the api, which then fires a browser redirect to the login page of the angular app. On the /login route, the login component needs to snapshot the url to grab the result of the confirmation from queryparams (true or false) and generates a toast accordingly - this happens in the ngOnInit() method.

I've also tried it in ngAfterViewInit()and ngAfterContentInit(), both throw the same error.

I don't really want to do the setTimeout hack, because I'm not convinced that is the correct solution (see this). So currently investigating alternatives.

I agree that it is a hack. It's a problem on Angular's end. I don't know how big of a priority it is to them since there's an easy workaround even if it is kind of a hack.

Not sure of your specific details and restrictions, but why not have the link go to a verification page on the angular app which then calls the API to do the verification? If successful show a toast and redirect to login otherwise show an error. That would avoid the issue all together.

Otherwise instead of using the router snapshot, you could subscribe to the observable and then grab the query params inside the subscription. This would cause the code that displays the toast to run after ngOnInit() finishes. However, this is basically the same thing as calling setTimout() in the first place.

@yarrgh Thanks for the suggestion, which I think would work...but I'm using a Rails gem [devise_token_auth] which generates the confirmation link automatically (https://github.com/lynndylanhurley/devise_token_auth) so not much I can do without figuring out how to override that mechanism.

I'll just go with the timeout hack since in production there won't be side effects as I'm not changing any of these values after the component loads (which is sort of what this error is warning against).

Looks like this should be fixed soon: https://github.com/angular/angular/pull/18352

Not sure which release it'll be included in.

Closing since there really isn't anything we can do about this right now

I am aware that this issue was closed without a solution or a proper solution. I believe sharing this could be helpful. It seems like the issue has something to do with angular life cycle hooks. I am yet to find time to learn about the details. For my case I moved this.toastrService.success(this.message); from ngOnInit() to constructor() as recommended here: https://stackoverflow.com/questions/43375532/expressionchangedafterithasbeencheckederror-explained

Was this page helpful?
0 / 5 - 0 ratings