简体   繁体   中英

Angular ExpressionChangedAfterItHasBeenCheckedError when passing data from Parent Component to Child Component using Map

I have a very simple setup. I have an input field in app.component.html in which the user can type in a string. I am adding that string to a Map<string, number>. I am passing that map to child component. I am accessing the map in child component using @Input() and iterating over it and displaying the values in child component.

app.component.html -> Parent Component

<div class="shopping-cart">
    <app-shopping-cart-item [shoppingItems]="shoppingItems"></app-shopping-cart-item>
</div>
<p>
  <button class="btn btn-primary" (click)="updateName()">Update Name</button>
</p>

app.component.ts

  shoppingItems = new Map<string, number>();

  updateName(){
    this.shoppingItems.set('test', 1);
  }

shopping-cart-component.html

<div *ngFor="let article of this.shoppingItems.entries()" class="shopping-cart-item">
  <div class="article-name">{{ article[0] }}</div>
</div>

Shopping-Cart-Item.component.ts

  @Input()
  shoppingItems;

The issue is when I enter a value in the input value (in parent) and click update then it updates the values displayed in the child component. But I get a ExpressionChangedAfterItHasBeenCheckedError.

I know that we get this error when we don't have unidirectional data flow. But I don't understand why I get this error in this scenario.

core.js:6210 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '[object Map Iterator]'. Current value: '[object Map Iterator]'.
    at throwErrorIfNoChangesMode (core.js:8129)
    at bindingUpdated (core.js:19991)
    at Module.ɵɵproperty (core.js:21142)
    at ShoppingCartItemComponent_Template (shopping-cart-item.component.html:3)
    at executeTemplate (core.js:12059)
    at refreshView (core.js:11906)
    at refreshComponent (core.js:13358)
    at refreshChildComponents (core.js:11635)
    at refreshView (core.js:11958)
    at refreshComponent (core.js:13358)

This is almost definitely caused by using the entries() function in template. I can't speak to the specifics of why this is causing the issue, as I don't know the inner workings of Map that well, but I'm assuming it's something along the lines of a new array (or Iterator) gets created everytime that function is called, and that function is called on every change detection cycle, which is making angular believe something is changing during the change detection cycle caused by the button click. Or perhaps Angular doesn't handle Itertors very well.

either way, using the keyvalue pipe to iterate instead should solve the problem, as it will only change the array when it needs to:

<div *ngFor="let article of this.shoppingItems | keyvalue" class="shopping-cart-item">
  <div class="article-name">{{ article.key }}</div>
</div>

the problem is the entries() function. That function internally creates an iterable object that it is changing each time you add something to it. It's known as bad practice to call functions from the component view so if you want to maintain the Map in you parent component then I recommend you that child component receives an array from the parent instead of a Map. To do so, you have to create an array property in parent component as Map and pass it to child component. Each time you execute updateName function you should maintain both, Map and Array.

  shoppingItems = new Map<string, number>();
  childArray = [];

  updateName(){
    this.shoppingItems.set('test', 1);
    this.childArray.push(this.shoppingItems.get('test'));
  }

A small commercial: There is a important situation with your updateName() function. First, it is not a parameterized function because each time you execute it you are adding the same thing. So, a Map is a structure which would not let you add more than once same key, in this case 'test' so that is replacing the same key value each time. In addition to that, when you would like to parameterize the function you have to be sure before that key is already added in array to avoid duplicating items and replace the index if apply. Just for your information. So, next.

Then, we have to change the child and parent views too:

Parent:

<div class="shopping-cart">
    <app-shopping-cart-item [shoppingItems]="childArray"></app-shopping-cart-item>
</div>
<p>
  <button class="btn btn-primary" (click)="updateName()">Update Name</button>
</p>

Child:

<div *ngFor="let article of this.shoppingItems" class="shopping-cart-item">
  <div class="article-name">{{ article }}</div>
</div>

So in that manner we are taking care of duplicates with Map and therefore in the Array too because we are depending on Map.

Try it.

Note: There is another important and advanced topic called Change detection .

Try like this. 

    import { Component, ElementRef, OnInit, ViewChild, ChangeDetectorRef, ViewRef } from '@angular/core';
    
      constructor(
        private dtr: ChangeDetectorRef,
      ) {
      }
// write this code where you want to detect change in browser
     if (this.dtr && !(this.dtr as ViewRef).destroyed) {
                this.dtr.detectChanges();
              }
updateName(){

setTimeout(() => {
this.shoppingItems.set('test', 1);   
},
 0);
     
}

try this, you should no more get that error (place the code within the function in setTimeout)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM