Aura Components are being aggregated on 'items' change of aura:iteration

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:

  <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>
  <ul aura:id="list">
    <aura:iteration items="{!v.mylist}" var="myitems">
        <li aura:id="item">{!myitems + ''}</li>

// 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);

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.

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.