3

I've created a simple component to show my issue:

Markup (testComp.cmp):

<aura:component description="testComp">
  <aura:attribute name="testAttr" type="List" default="[[[1, 2], [2, 3], [3, 4]]]" />
  <button onclick="{!c.doSomething}" />
  <aura:iteration items="{!v.testAttr}" var="testVal">
    <aura:iteration items="{!testVal}" var="innerVal">
      <div aura:id="innerTestDiv" />
    </aura:iteration>
  </aura:iteration>
</aura:component>

JSController (testCompController.js):

({
  doSomething : function(component) {
    var items = component.find('innerTestDiv');
    alert('Total components: ' + items.length);
    var test = component.get('v.testAttr');
    component.set('v.testAttr', test);
  }
})

Every time I click the button and set the value of "testAttr" again for the aura:iteration to iterate over it, the amount of objects returned from component.find('innerTestDiv') is increased, because the old components aren't being deleted and still seem to be on the DOM.

I've tried setting testAttr to null or an empty array before resetting it, but it doesn't help.. the moment that component.set('v.testAttr', test) is executed, the aura:iteration creates a new set of components.

Is there any way to clear the old components/elements so that only the new ones will be displayed?

I need it because I'm trying to dynamically collect the data from all the fields on the screen and currently I've done it by embedding components inside other components and just gathering information from them.. but once I encounter this issue of reiterating again, I can't rely on the created components any longer.

  • 1
    Look like a bug. Please raise a case with the support – Praveen Aug 23 '16 at 17:03
  • See if my updated answer works - I think I have a solution. – Caspar Harmer Aug 24 '16 at 9:12
  • Ok, updated again... it's not great, but I have managed to get around the problem, sort of. – Caspar Harmer Aug 24 '16 at 17:25
4

Thank you for the demo code. I have confirmed the problem and filed a bug. Here are the conditions to get the component leak:

  1. LockerService enabled
  2. Nested <aura:iteration>
  3. The inner elements of the iteration has arrays.

In other words, [[1,2,3]] is fine but [[ [ ], [ ], [ ] ]] leaks the elements in the second iterations 3 times, [[ [ ], [ ], "A"]] leaks twice etc.

The good news is that objects are not affected, [[ { }, { }, { } ]] is not leaking, so you can change the inner structure to an object.

Here is the repro code if you want to play with different scenarios:

// test.app
<aura:application>
  <aura:attribute name="mylists" type="List"/>
  <aura:attribute name="mylist" type="List"/>
  <aura:handler name="init" value="{!this}" action="{!c.init}"/>
  <button onclick="{!c.test}">Test</button>
  <aura:iteration items="{!v.mylists}" var="mylist">
    <ul aura:id="list">
    <aura:iteration items="{!mylist}" var="myitems">
        <li aura:id="item">{!myitems + ''}</li>
    </aura:iteration>
    </ul>
  </aura:iteration>
  <ul aura:id="list">
    <aura:iteration items="{!v.mylist}" var="myitems">
        <li aura:id="item">{!myitems + ''}</li>
    </aura:iteration>
  </ul>
</aura:application>

// testController.js
({
  init : function(component) {
      // component.set('v.mylists', [['A', 'B', 'C'], ['a', 'b', 'c']]); // No leak
      // component.set('v.mylists', [['A', ['Y', 'B'], 'C'], ['a', 'b', 'c']]); // Leaks 1 component
      // component.set('v.mylists', [[['X', 'A'], ['Y', 'B'], 'C'], ['a', 'b', 'c']]); // Leaks 2 component
      component.set('v.mylists', [[['X', 'A'], ['Y', 'B'], ['Z', 'C']], ['a', 'b', 'c']]); // Leaks 3 component
      // component.set('v.mylists', [[[], [], []], ['a', 'b', 'c']]); // Leaks 3 component
      // component.set('v.mylists', [[{}, {}, {}], ['a', 'b', 'c']]); // No leak
      // component.set('v.mylists', [[{length: 0}, {length: 1}, {length: 2}], ['a', 'b', 'c']]); // No leak
      component.set('v.mylist', [['X', 'A'], ['Y', 'B'], 'C']); // No leak
  },
  test : function(component) {
    var list = component.find('list');
    var items = component.find('item');
    alert('Total lists: ' + list.length + "\n" + 'Total list items: ' + items.length);
    var mylists = component.get('v.mylists');
    component.set('v.mylists', mylists);
    var mylist = component.get('v.mylist');
    component.set('v.mylist', mylist);
  }
})
2

For the case of click, localId deIndexing on component only happens during rendering cycle, which is after the client action is done. During rendering cycle, the framework will destroy unneeded component and deIndex the localId from the owner component. This is because framework wants to reuse the components which have same value in the old list. Component.set() does not clean up the localId indexes on the component, but it marks the component as dirty for re-render.

For the case of click2, the callback gets executed asynchronously, it gets called after the rendering cycle completes. That's why you can see the up to date count.

For workaround, I think Luke's comment above could work, but only if the new list does not contain any value in the old list. As I said, if the value exists in the old list, the component with the value will be reused.

  • Hi cheng, welcome to SFSE. Please take a moment to scroll through the tour and read How to Ask and How to Answer. It seems like you are trying to provide an answer, but it is hard to tell because you ask questions in your post also. Please try to avoid asking any questions in an answer post. It will often get answers deleted if they seem more like follow up questions. Once you have enough reputation (50) to leave comments, you'll have a good place to post such questions. Until then, best to avoid. – Adrian Larson Jan 31 at 22:33
1

A simpler workaround that worked for us: Check to see if the value returned from isRendered() on each of the components returned from component.find() is true. For us all the 'old' ones were always false.

0

EDIT

Ok, Turns out that didn't work.

The best I've been able to do is define all the inner arrays as top level arrays. So if you have three levels deep, you define three attributes. Which is painful, I know.

Then you set these arrays separately. Each time you get describe information, you'd need to parse out the arrays and set each level.

Working code for reference:

<aura:application >
    <aura:attribute name="testAttr1" type="List" default="[1,2]" />
    <aura:attribute name="testAttr2" type="List" default="[3,4]" />
  <button onclick="{!c.doSomething}" />

  <aura:iteration aura:id="iterator" items="{!v.testAttr1}" var="var1">
      <div aura:id="outerTestDiv" >{!var1}</div>
      <aura:iteration items="{!v.testAttr2}" var="var2">
          <div aura:id="innerTestDiv" >{!var2}</div>
      </aura:iteration>
  </aura:iteration>

</aura:application>

JS:

({
  doSomething : function(component) {
    var items = component.find('innerTestDiv');
    alert('Total components: ' + items.length);
    var test = component.get('v.testAttr2');
    component.set('v.testAttr2', test);
  }
})

Whether that's good enough, I don't know, but I haven't been able to improve on it.

  • 1
    Thanks, yeah, I did mean to use three arrays unfortunately. In my original code I'm iterating over all 3 of them, but I didn't do it here as the issue replicates without it and I didn't want to overcomplicate the example. – Poster Aug 24 '16 at 4:55
  • So, just so I can replicate, you basically have one more aura:iteration - anything else that might be handy for replication? (I may as well get it as close as possible to your code - I'm kind of keen to see what is going on) – Caspar Harmer Aug 24 '16 at 5:28
  • In my original code I have 3 arrays and 3 iterations (one over each array, when the array is the var of the previous iteration, just like in the example). The second iteration (I didn't check the third, didn't get to that point as it was not yet relevant to my case due to the problem with the second) keeps generating new components without getting rid of the old ones. Eventually what I want to do is do component.find() on the most inner array's results and retrieve data from there, but since #2 aggregates (and I think #3 as well), I end up with too many components on the component.find(). – Poster Aug 24 '16 at 5:39
  • The business logic is me iterating over the result of the Metadata API's describeLayout() (which is a Page Layout). It gives me 1 Section (First 'array'), with 6 Rows(Second array), with up to 2 Columns each (Third array). And then I'm retrieving the column data (which have the actual aura:id).. all of this is inside a generic nested component, so I'm doing component.find('layoutcmp').find('columns') and expect to get the columns, but getting more than expected due to the given issue. * In this example it would be 1(section) - 3(rows) - 2(columns). – Poster Aug 24 '16 at 5:44
  • @CasparHarmer component.find('innerTestDiv') would return an Array, so set() won't be available.Iterating over the component array and setting it's body to [] would work. – Praveen Aug 24 '16 at 9:46
0

Here's the repro of the issue in 2018 and a workaround we can live with. Your mileage may vary, of course.

Compare onClick to onClick2. The later will produce correct counts in contrast to onClick.

<aura:application >
    <aura:attribute name="myList" type="List" default="[7,8,9,10]"/>    


    <aura:iteration items="{!v.myList}" var="item1">
        <div aura:id="listItem1">{!item1}</div>
    </aura:iteration>

    <lightning:button onclick="{!c.onClick}" label="click1"/>
    <lightning:button onclick="{!c.onClick2}" label="click2"/>

</aura:application>
({

    onClick : function(component, event, helper) {
        var count = component.find("listItem1").length;
        console.log("1: "+count);
        component.set("v.myList", [11,12]);
        count = component.find("listItem1").length;
        console.log("2: " +count);
    },

    onClick2 : function(component, event, helper) {
        var count = component.find("listItem1").length;
        console.log("1: "+count);
        component.set("v.myList", [11,12]);
        var timer = window.setInterval($A.getCallback(function() {
                if (component.isValid()) {
                   clearInterval(timer);
                   count = component.find("listItem1").length;
                   console.log("2: " +count);
                }
        }), 200);
    }

})

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.