4

I have a lightning application that is being used in both lightning and classic (via ltng:outApp).

I have a series of callbacks that work perfectly in lightning, but when invoked from classic, the second action.setCallback() callback is never called, and when I monitor action.getstate() it returns "NEW" indefinitely.

Right now, the first call to the "c.getMailingPeriods" controller action works and the data is returned.

Then, the call to "c.getQuoteLines" is called and enqueued, but is never executed and the callback is never called.

If I first call "c.getQuoteLines", then it works and the subsequent action then never gets executed. (I have confirmed the params are all set properly with the correct values)

I have attempted to wrap in $A.setCallback(), without success.

Again, this works in lightning, but in classic the enqueued action never gets attempted to be submitted to the server.

Controller

...
helper.getMailingPeriods(component)
.then(function(mailingPeriods) {
    component.set('v.mailingPeriods', mailingPeriods);

    // Get quote lines for bundle, with subquery of previously selected NTAs
    helper.getQuoteLines(component, quoteId, quoteLineId)
    .then(function(quoteLines) {
        ...

Helper

...
getQuoteLines: function(component, quoteId, quoteLineId) {
    return new Promise(function (resolve, reject) {
       // Get jobs from controller
       var action = component.get("c.getQuoteLines");

        action.setParams({
            "quoteId":  quoteId,
            "bundleQuoteLineId": quoteLineId,
        });

       action.setCallback(component, function(results) {
           utils.log('getQuoteLines() results', results);
           if(results.getState() === 'SUCCESS') {
               utils.log('results:', results.getReturnValue());
               resolve(results.getReturnValue());
           } else if (results.getState() === 'ERROR') {
               utils.log('getQuoteLines() ERROR', results.getError());
               $A.log("Errors", results.getError());
               reject(results.getError());
           }
       });

       $A.enqueueAction(action);

   });
},

getMailingPeriods: function(component) {
    return new Promise(function (resolve, reject) {
       // Get jobs from controller
       var action = component.get("c.getMailingPeriods");

       action.setCallback(this, function(results) {
           utils.log('getMailingPeriods() results', results);
           if(results.getState() === 'SUCCESS') {
               utils.log('results:', results.getReturnValue());
               resolve(results.getReturnValue());
           } else if (results.getState() === 'ERROR') {
               utils.log('getMailingPeriods() ERROR', results.getError());
               $A.log("Errors", results.getError());
               reject(results.getError());
           }
       });

       $A.enqueueAction(action);
   });
},
...
2
  • I had a problem of lost callbacks when the requesting component was replaced in the body of another component before the callback had completed. Mentioning it here in case there is some marginal timing in your app that only surfaces in the Lightning Out case.
    – Keith C
    Jul 11, 2018 at 8:35
  • Thanks for the input. I don't think that I have anything replacing the content of my component, as this is the very top level component. I noticed some odd behavior, when I refresh the page, I can see that my debug entries show up momentarily right before the refresh happens (meaning that the callback finally get's called and is returned).... I am not sure what might be holding it up from executing. Jul 11, 2018 at 13:59

3 Answers 3

3

It appears that helper.getQuoteLines( should have a return in front of it so that the promise chain resolves correctly. As written, getQuoteLines will not have resolved when the following .then( is evaluated.

Maybe it is just because there is so much of the controller code trimmed - update the question if this assumption is incorrect.

I would expect that the chain might look similar to this:

helper.getMailingPeriods(component)
    .then(function (mailingPeriods) {
        component.set('v.mailingPeriods', mailingPeriods);

        // Get quote lines for bundle, with subquery of previously selected NTAs
        return helper.getQuoteLines(component, quoteId, quoteLineId)
    })
    .then(function (quoteLines) {
        // quotelines stuff
        return resultOfQuoteLinesStuff;
    })
    .then(function (resultOfQuoteLines) {
        // anything you needed to do with the results of the quote lines
        return foo;
    })
    .catch(function (err) {
        console.log(err);
    });
1
  • 1
    Thank you for the suggestion. I am not chaining .then's together and all of my logic is nested in each function, and that should not matter as it is identical. The problem is that SFDC never calls the callback from action.setCallback() - SFDC never sends the job to the server. I have used setTimeout() to monitor the action using action.getState() and it stays at "NEW" forever, which tells me the problem is related to the Aura framework and not related to the promises. Jul 11, 2018 at 0:38
2

Posting as an answer because I don't have the reputation to comment. Austin's answer helped to resolve this issue for me as well, and after some googling I was able to put some pieces together as to why $A.getCallback() is needed. The basic information can be found in this post:

What does $A.getCallback() actually do?

Evidently there is a "rendering cycle" where lightning rendering will occur as attributes are changed etc. This cycle is presumably started when a lightning-supported event occurs (e.g. a button click defined in the component XML). However if an event is triggered from something outside of the framework, such as a setTimeout or a promise (which is built on things like setTimeout), it will not complete. As a result, callbacks handling unsupported events need to be wrapped with $A.getCallback() to kick off a rendering cycle.

The impact of a rendering cycle appears to go beyond component rendering; in my case, as above, it prevented a server call from firing. It seems to be an overall "on" switch to the lightning framework, presumably for purposes of efficiency, though that is speculative. Best practice seems to be to always wrap promise and setTimeout callbacks with getCallback, to guarantee that the environment is "on".

As for why setTimeout and promises sometimes (often?) work without getCallback, I suspect this is happening because they are borrowing the rendering cycle of another event. When I first started seeing this issue, I was able to almost always prevent it by having a setTimeout value of 10 milliseconds, then reproduce it by using a setTimeout value of 2000 milliseconds. This behavior suggests that the rendering cycle of the code that first enqueued the timeout was still running after 10 milliseconds (but not after 2000); thus it was able to be used when the timeout completed. Depending on how long a timeout or promise takes to resolve (and how long it takes a rendering cycle to complete), the callback therefore may or may not be able to use the rendering cycle of the original code, causing intermittent results.

1

After a long trek with SFDC support (involving refactoring my code to reproduce the issue with less than 200 lines of code so they were allowed to "look at it"), I was able to figure out a solution, but I am still unclear why this is an issue here and not in other places in the codebase (or other codebases I have built in classic to work with lightning).

The issue was that when a promise is used, the promise anonymous function should be wrapped in $A.getCallback() to ensure it will work in all cases. I am not sure why I have never had problems in the past without wrapping in $a.getCallback().

For reference - https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_promises.htm

({
    getMailingPeriods: function(component) {
        return new Promise($A.getCallback(function (resolve, reject) {
           var action = component.get("c.getMailingPeriods1");
           action.setCallback(this, function(results) {
               console.log('getMailingPeriods() results', results);
               console.log('results.getState():', results.getState());
               console.log('results.getReturnValue():', results.getReturnValue());
               resolve(results.getReturnValue());
           });
           $A.enqueueAction(action);
       }));
    },
    getQuoteLines: function(component, quoteId, quoteLineId) {
        return new Promise($A.getCallback(function (resolve, reject) {
            var action = component.get("c.getQuoteLines");
            action.setParams({ "quoteId":  quoteId, "bundleQuoteLineId": quoteLineId, });
            console.log('params - sanity check', { "quoteId":  quoteId, "bundleQuoteLineId": quoteLineId, });
            action.setCallback(this, function(results) {
               console.log('getQuoteLines() results', results);
               console.log('results.getState():', results.getState());
               console.log('results.getReturnValue():', results.getReturnValue());
               resolve(results.getReturnValue());
            });
            console.log('action about to be enqueued');
            $A.enqueueAction(action);
            console.log('enqueue action method called');
       }));
    },
})

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .