[SalesForce] Application event fired multiple times – multiple copies of components in memory

I'm using an application event to broadcast a global change. The event is fired once yet several handlers are triggered multiple times. All components are created purely via markup (e.g. none are generated in code).

This same problem was reported here https://developer.salesforce.com/forums/?id=9060G000000IAM6QAO back in March 2017. Two people commented they had a similar problem but no one offered a solution. I also found this and this. In both of these questions Rohit Kandul offered a solution but no one has confirmed the solution is ok.

  1. what is the lifecylce of components created via markup.
  2. when are components destroyed? or better when are their event handlers disabled.
  3. is it appropriate and is it safe to use Rohit's solution. Which is:

In the RENDERER file put the below code

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

UPDATE:

Background information that may help. I've created a "single component app" for Cases. It contains many child components all declared in the markup. The main component is placed on the Case record page and supports staff who manage emails and case resolution. When the user selects a Case from the Case list view the Case record page is displayed and my component hierarchy is created.

I assumed these components would only be created and initialized once. But each time the user selects a new Case a whole new set of components are … I'm not sure what to say but I know that each's init handler is called.

The multiple event problem above is simply because there are multiple components, in memory, that are duplicates, that are registered to handle the application event.

UPDATE2
Each component has a unique id. Using this

 var thisId = component.getGlobalId();

I can see that only four copies of each component are present in the system. I can also see they all share the same data. Why does LE need duplicates of what are essentially singletons? Why create a new Javascript object that shares data with it's brethren? The end result is there are four copies of every component in my system and since they respond to application events and make server calls there is a great deal of wasted resources and sluggishness built into the system. I can see that we won't run out of memory.

UPDATE 3

Some have suggested to manually destroy these duplicate instances based on the location change event. But it seems from here that this event is flakey and unpredictable and from here there is a known issue with location change and IE11.

It also seems from here that manually destroying these duplicate instances is also flakey and unpredictable. (And besides why should the developer be messing with the framework this way?)

I've also found another original question that is a duplicate of mine here

And another duplicate here

Update 4:

I was at first very hopeful with Praveen's answer and that he had help from SF but it does not work because the duplicate components actually share the same data. Below are simplified debug statements from one component as it responds to application events GC, AL, EE and EV, plus the init event IN.
For IN there is only the record id as passed into the component via markup <c.mycmp recordId=...>. The next id is the record id coming from the event from the component that emits the event. Which, of course, is its own copy of record id that it got from initialization. Component cmpA is initialized first. It get events GC and AL. I then select another tab with another record and cmpB is initialized. See how cmpA still responds to the events but now it's local copy of record id has been replaced. I was very careful to stringify all debug statements so that we don't have the browser showing us live data.
I then click on a third tab and we have three active components: cmpA, cmpB and cmpC ALL with the same record id!!!!

IN  "cmpA, id1"
GC  "cmpA, id1, id1
AL  "cmpA, id1, id1


IN  "cmpB, id2"
GC  "cmpA, id1, id2"
GC  "cmpB, id2, id2
AL  "cmpA, id2, id2
AL  "cmpB, id2, id2
AL  "cmpA, id2, id2
AL  "cmpB, id2, id2

IN  "cmpC, id3"
GC  "cmpA, id2, id3"
GC  "cmpB, id2, id3"
GC  "cmpC, id3, id3
AL  "cmpA, id3, id3
AL  "cmpB, id3, id3
AL  "cmpC, id3, id3
AL  "cmpA, id3, id3
AL  "cmpB, id3, id3
AL  "cmpC, id3, id3
AL  "cmpA, id3, id3
AL  "cmpB, id3, id3
AL  "cmpC, id3, id3

UPDATE 5

I may have found a solution. That GC event represents an application event that broadcasts global data is ready. This data contains the Case Number. (I'm just working with Case records.) Even though all three visible components are showing the same record id (which is just wrong) they do show different and correct case numbers. Now if I can pass the case number in the events I might be able to determine which component should process which event. I can't explore this further for now but it might help to consider what is different between the record id and case number. The record id is set via markup as an attribute of the component; passing the record id from the parent down to the child. In my situation, the case number is coming via a broadcast event when global data is ready. The child component receives this data and stores a piece of it (the case number) in a component attribute. Somehow this round about way of setting the value is different. Probably because LEX is creating bindings between the markup declared attribute value. Yes. this might be the next stage of exploration.

UPDATE 6 Greg Grinberg suggests passing in the case ID via an UNbound expression to avoid the data binding {#case.id}

Best Answer

I posted the same question in Lightning Component group in the partner community and got the below response from the salesforce employee:

In a LEX app that uses the console layout, each page's contents are maintained in the DOM and in memory at the same time, making it possible to switch between workspace tabs by merely updating CSS instead of recreating components and rerendering them.

In a LEX app that uses the standard layout container, each page's contents are also maintained in the DOM and in memory, but the rendered markup of components from pages that the user previously visited is hidden via CSS. This is all managed by a special layout component that maintains a set of components keyed by the page identifier (i.e. its URL). This layout allows the application to handle back/forward traversal through the browser history by merely changing CSS. In some cases, it also helps maintain scroll position, but that's not guaranteed or officially supported presently.

In both layouts, for instance, it is possible that a component will be instantiated twice, once for 2 distinct record pages, each for a different record. Developers have typically worked around this scoping issue where their components on other pages, which are currently hidden, will receive an event fired by a component on the current page and which is intended for other components on that same page.

The workaround usually involves adding a piece of data to the event payload that is unique to that page, and recordId is typically what's used. Handlers may then check that parameter when they receive the event and compare it against their recordId to see if it matches before actually doing work.

There is currently no platform-supported mechanism that builds in the concept of a page-scoped event explicitly. That may change in the future, but adding information to the event that enables the components to scope it themselves will often suffice.

In either case, adding code to a component that manually destroys itself is not a good idea and will often lead to problems.

If you assign the recordId via markup (e.g. in the parent component <c.childCmp recordId="{!v.recordId}"/> then it is important to stash a copy of the record id in another attribute in the child. This is because the LEX system binds the recordId value in the parent to the child and when the parent's recordId is changed so will the children. But if you create <aura:attribute name="myCaseId" type="String" /> and copy the incoming record id into this myCaseId property, in the init handler, then you will have a safe copy of the record id to use when all the events come along.

Related Topic