16

In the Lightning Component Developer Guide you can find that Promises can be used in Lightning Components and what special things we need to care for to make them work. But there is no fully working example on how to implement and use them.

Let's say we have 3 @AuraEnabled Apex methods we want to call asynchronously after each other using the output of the first as input parameter of the second and so on.

public class elfApexCascade {
    @AuraEnabled public static Object myServerSideAction_1(String input) {
        return input+'.Apex1';
    }
    @AuraEnabled public static Object myServerSideAction_2(String input) {
        return input+'.Apex2';
    }   
    @AuraEnabled public static Object myServerSideAction_3(String input) {
        return input+'.Apex3';
    }   
}

The logic is very simple: it concatenates only the strings and calling the first method with an initial "START" we would expect something like this as result of the last Apex call: "START.Apex1.Apex2.Apex3"

The component itself has only one button to start the cascaded execution.

<aura:component controller="elfApexCascade" implements="flexipage:availableForAllPageTypes,force:hasRecordId" access="global" >
    <button onclick="{!c.onclick}">Apex</button>
</aura:component>

Now invoking that Apex in good old xmas-tree-style looks like this in the controller:

onclick : function(cmp, evt, hlp) {
    var call1 = cmp.get("c.myServerSideAction_1");
    call1.setParams( { input : 'START' } );
    call1.setCallback(null,function(res) {
        if(res.getState()=='SUCCESS') {
            console.log('Call 1 : ', res.getReturnValue() ); 
            var call2 = cmp.get("c.myServerSideAction_2")
            call2.setParams( { input : res.getReturnValue() } );
            call2.setCallback(null,function(res) {
                if(res.getState()=='SUCCESS') {
                    console.log('Call 2 : ', res.getReturnValue() ); 
                    var call3 = cmp.get("c.myServerSideAction_3")
                    call3.setParams( { input : res.getReturnValue() } );
                    call3.setCallback(null,function(res) {
                        if(res.getState()=='SUCCESS') {
                            console.log('Call 3 : ', res.getReturnValue() ); 

                            // ... optionally more nestings here

                        }
                    });
                    $A.enqueueAction(call3);
                }
            });
            $A.enqueueAction(call2);
        }
    });
    $A.enqueueAction(call1);        
},

If you press the button it executes in the console like this:

Call 1 :  START.Apex1
Call 2 :  START.Apex1.Apex2
Call 3 :  START.Apex1.Apex2.Apex3

Can we use Promises for that scenario in order to improve readability and maintainability of that code?

  • 1
    Another example here – martin Sep 8 '17 at 2:27
17

Using Promises, at least we can make it look like that in the controller:

onclick : function(cmp, evt, hlp) {
    hlp.apex(cmp,'myServerSideAction_1',{ input : 'START-PROMISE' })
        .then(function(result){
            console.log('Call 1 : ' , result );
            return hlp.apex(cmp,'myServerSideAction_2',{ input : result });
        })
        .then(function(result){
            console.log('Call 2 : ' , result );
            return hlp.apex(cmp,'myServerSideAction_3',{ input : result });
        })
        .then(function(result){
            console.log('Call 3 : ', result );
        })
        // optionally more chainings here
    ;
},

In order to make it work we need to wrap the apex invocation in a promise. We can do it easily in the helper, however this pattern is re-usable over many components and we might want to move it out into a separate JS Static Resource later. In the helper it would be:

apex : function( cmp, apexAction, params ) {
    var p = new Promise( $A.getCallback( function( resolve , reject ) { 
        var action                          = cmp.get("c."+apexAction+"");
        action.setParams( params );
        action.setCallback( this , function(callbackResult) {
            if(callbackResult.getState()=='SUCCESS') {
                resolve( callbackResult.getReturnValue() );
            }
            if(callbackResult.getState()=='ERROR') {
                console.log('ERROR', callbackResult.getError() ); 
                reject( callbackResult.getError() );
            }
        });
        $A.enqueueAction( action );
    }));            
    return p;
},

I think the main benefit is chaining promises without that ugly nesting in the vanilla-style. At the same time, we can get rid of that nasty "c." thing in front of the method name and do us a favor by letting us set the parameters without the verbosity of action.setParams({}). All of this combined is shrinking the code of the business-logic a lot. Also nice is a new way of error-handling.

$A.getCallback()

It is very important to point out the use of $A.getCallback() as stated in the documentation. If you omit it (like I mistakenly did the first time!), you might not even notice it immediately. But you will run into very hard to debug issues later on. Without $A.getCallback() it even worked most of the time, but it felt like having performance issues and the chained invocations came back seriously delayed or only after clicking around on the UI. It led me on a wrong path while asking this question: Lightning Components: why are subsequent invocations of $A.enqueueAction() so super SLOW?

But it had nothing to do with server-side performance or penalties for subsequent calls. It was simply the missing of $A.getCallback()

  • Does resolve call another method? A bit confused on how this is different than the use of getCallback....seems to me it is just adding code.... – Eric Sep 8 '17 at 8:54
  • @Eric I don't know yet. The only thing I changed is what they are telling here developer.salesforce.com/docs/atlas.en-us.lightning.meta/… - without doing any research on the "why". I was very happy that it worked and solved this big issue for me and I had simple not enough time to investigate yet. But I think this is about the backgrounds: developer.salesforce.com/docs/atlas.en-us.lightning.meta/… – Uwe Heim Sep 8 '17 at 21:00
  • For SF documentation on what $A.getCallback is doing differently from .setCallBack, see the note at the bottom of this link: developer.salesforce.com/docs/atlas.en-us.lightning.meta/…. I'll post the quoted text as a followup comment – smohyee Oct 9 '18 at 19:02
  • "The two callbacks are wrapped by $A.getCallback() in our example. What’s that all about? Promises execute their resolve and reject functions asynchronously so the code is outside the Lightning event loop and normal rendering lifecycle. If the resolve or reject code makes any calls to the Lightning Component framework, such as setting a component attribute, use $A.getCallback() to wrap the code." – smohyee Oct 9 '18 at 19:03

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.