19

I have a couple custom lightning pages and apps, both overriding default record views and standalone pages (just straight nav to from the sidebar). It seems that every time we go from 'tab' to 'tab' it just hides the content rather than destroying it. When you navigate back to the tab, it does recreate the components, so you end up with 2 of everything sitting out there.

I noticed this b/c we have a component that registers to an app event. And every time you navigate away from and back to the page, the handler is registered +1. So going back and forth a few times you pretty quickly have 10s of the same handlers firing for each app event.

Anyone seen this? Thoughts?

If you create a Lightning App, add this component, and assign it to lightning menu you can see what I mean. Just go the app click home and then click back:

Comp:

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes">
    <aura:handler event="c:BoringAppEvent" action="{!c.test}"/>
    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:BoringAppEvent');
         notEvt.fire();
    }
})

Renderer

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

BoringAppEvent.evt

<aura:event type="APPLICATION" description="Event template" />

enter image description here

  • Are you creating your lightning components dynamically using createComponent ? – Cloud Ninja Aug 24 '16 at 23:32
  • I am not. It's all mark up. – wellmstein Aug 25 '16 at 1:43
  • 2
    Ok Interesting twist...it seems to start unrendering after 3 stacks: – wellmstein Aug 25 '16 at 2:26
  • I think this event should have an option to destroy previous component.. – dzh Feb 6 '18 at 11:29
2

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');
    }
})
| improve this answer | |
  • Just seeing this. I haven't tried it, but it's a good idea that I assume will work...so you're getting the green check. If I get around to trying it, will post back. – wellmstein Apr 13 '17 at 15:37
  • I would you advice you mark the answer as accepted only if it solves your issue. – Praveen Apr 13 '17 at 18:44
  • Just an update on this: As per this documentation aura:locationChange is not going to work if URL does not have hash tag. I think it should not matter for navigation that is happening from code, but should impact navigation on tab change. Please let me know views on this. – Sarang May 28 '18 at 7:19
  • 1
    I can confirm that using locationChange is not reliable. Further, destroying the components is very risky and not advisable. Another approach is to add some checks into the event handlers to only process calls that come from the component's family. See alternative address below. – Bryan Jul 7 '18 at 3:01
  • 1
    @Suneel. aura:location change doesn't capture all page navigations. As per the documentation it's meant to handle changes to the hash part of the url. Also, LEX reuses components and it destroys them when it knows it's done with them. If we destroy components we may break LEX in some strange and difficult to diagnose way. Also, we'd be creating code that is fragile when LEX is updated by SF. – Bryan Jan 2 '19 at 19:03
12

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.

| improve this answer | |
  • Is there a way to destroy your components when it's not visible? All of those solutions feel like they go against what LEX development should be like.. – Jason Lee Aug 31 '16 at 16:17
  • When tried the first method I run into the below Uncaught Error in $A.getCallback() [TypeError: Can't add property theOneTrueHandler, object is not extensible] – Surya Chandra Rao Gandreddi Sep 13 '16 at 8:13
  • I see four copies in my Summer 18 dev ed. When I hit the back button my components do not revert to any previous state. In fact they display the same content as before the user pressed 'back'. In other words, I've another problem to solve now. – Bryan Jul 4 '18 at 14:08
  • Perhaps I've created a duplicate question here: salesforce.stackexchange.com/questions/223806/… – Bryan Jul 4 '18 at 14:09
1

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.

| improve this answer | |
0

i was able to solve this issue today

issue - The old components instances listening to the event fire.

Solution - While we move away from a particular component we need to destroy that components as Salesforce stacks the instances.

In the RENDERER file put the below code

({ unrender: function(component){ this.superUnrender(); component.destroy(); } })

| improve this answer | |
  • I found the unrender event is not invoked – Bryan Jul 4 '18 at 14:06

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.