Navigation in Desktop Lightning Experience not Destroying Components

Ok - So...The deal is that the Lightning Experience tries to speed up your backbutton by saving the last 5 entries in the stack for history purposes. That means, if you click your tab, click away, click your tab again, you now have 2 of your entries in the stack.

If instead you click your tab, click away, then click the BACK button on your browser you are warped back to your original stack entry in the cache. zoom.

found that in app.js (lightning debug mode has comments)

 /**
         * sets the body of the content part of the center stage, and make sure we only keep this.MAXSTAGESLIDECOUNT (5) in
         * memory at the same time
         */
        setContentBody:  function(component, content, body) {
            //
            // We dont allow more than five history contents to be stored in memory.
            // The number five is based on common use case, and Diego's
            // performance evaluation.
            // If we go back to a step that has been destroyed,
            // we will recreate it based on the history stack array.
            //
            if (body.length > this.MAXSTAGESLIDECOUNT) {
                var skipped = body.splice(body.length - 2,1)[0]; //destroy previous to the last
                skipped.destroy(true);
                component._skipped++;

                content.set("v.body",body);
            }
        },

Reading the surrounding code/comments is very interesting as well.

We've identified a few workarounds.

  1. When your component with the handler inits, register to some global spot like

    $A.theOneTrueHandler = cmp;

    Then everytime you handle the event check before doing your stuff

    cmp === $A.theOneTrueHandler

  2. Parent Component passes his id downwards into the child components, then everyone uses that ID when dispatching application events and only handles the application event if the id matches. (probably the worst option)

  3. Find a way to use component events only.


If LEX is not destroying the component upon Navigation, then destroy the component yourself when the Navigation happens by adding aura:locationChange

All did is added aura:locationChange to your code, upon the location change the handler call which in turn destroys the component using : component.destroy()

Eg:

Component:

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes">
    <aura:handler event="c:BoringAppEvent" action="{!c.test}"/>
    <aura:handler event="aura:locationChange" action="{!c.destoryCmp}"/>
    hi
    <ui:button press="{!c.onClick}" >App Event</ui:button>
</aura:component>

Controller:

({
    test : function(component, event, helper) {
        console.log('Clicked');
    },
    onClick : function(cmp, evt){
         var notEvt = $A.get('e.c:BoringApp');
         notEvt.fire();
    },
    destoryCmp : function (component, event, helper) {
        component.destroy();
    },
})

Renderer :

({
    unrender: function(){
        this.superUnrender();
        console.log('unrender');
    }
})

I've been exploring this question in another question here and found a solution to my problem. Trying to destroy these components is not a good idea because they may actually be used by the LEX system again. Our real problem is around the event handling. We need to prevent event handlers from responding to events that are not intended for them. With @Praveen we've come up with a solution.

  1. when declaring child components pass in the parent's record id.
  2. in each child's init handler take the record id and copy it into another attribute. This is important because the record id property in the child record is bound to the property in the parent. When these duplicate components are created the record id is set to be the most recently active record. In other words, all components eventually get the same record id via the chain of bound properties. For example, the parent component

the parent

<aura:component controller="yourController" implements="force:hasRecordId" >
    <aura:attribute name="recordId" type="String" />
    <c:childCmp recordId="{!v.recordId}"/>

Then in the child

<aura:component>
<aura:attribute name="recordId" type="String" />    
<aura:attribute name="myRecordId" type="String" /> 
<aura:handler name="init" value="{!this}" action="{!c.doInit}" />

In the child controller

doInit: function(component,event,helper) {
    var recordId = component.get("v.recordId");
    component.set("v.myRecordId", recordId);

Next every time you fire the event send along the invokers copy of the record id that was stored during component initialization.

    var myAction = $A.get("e.c:myAction");
    myAction.setParams({"myRecordId": component.get("v.myRecordId")});
    myAction.fire();

Finally, in each event handler compare the event's recordId to your component's stored id and only take action if the event is coming from a component in your component's family.