So, what are promises?
In short, it’s a syntax that allows you to specify callbacks that should execute when a function either ’succeeds’ or ‘fails’ (is resolved, or rejected, in Promise terminology).
For many, they're a way of implementing callbacks in a way that makes a little more sense syntactically, but for others it's a new way of looking at how asynchronous code can be structured that reduces the dependancies between them and provides you with some pretty clever mechanisms.
However, this article isn’t about what promises are, but rather:
How can Promises be used in Lightning Components, and why you would want to?
If you want some in depth info on what they are, the best introduction I’ve found is this article on developers.google.com
In addition, Salesforce have provided some very limited documentation on how to use them in Lightning, here.
Whilst the documentations's inclusion can give us hope (Salesforce knows what Promises are and expect them to be used), the documentation itself is pretty slim and doesn’t really go into any depth on when you would use them.
When to use Promises
Promises are the prime candidate for use when executing anything that is asynchronous, and there’s an argument to say that any asynchronous Javascript that you write should return a Promise.
For Lightning Components, the most common example is probably when calling Apex.
The standard pattern for Apex would be something along the lines of:
getData : function( component ) { let action = component.get(“c.getData"); action.setCallback(this, function(response) { let state = response.getState(); if (state === "SUCCESS") { let result = response.getReturnValue(); // do your success thing } else if (state === "INCOMPLETE") { // do your incomplete thing } else if (state === "ERROR") { // do your error thing } }); $A.enqueueAction(action); }
In order to utilise Promises in a such a function you would:
- Ensure the function returned a Promise object
- Call 'resolve' or 'reject' based on whether the function was successful
getData : function( component ) { return new Promise( $A.getCallback( ( resolve, reject ) => { let action = component.get(“c.getData"); action.setCallback(this, function(response) { let state = response.getState(); if (state === "SUCCESS") { let result = response.getReturnValue(); // do your success thing resolve(); } else if (state === "INCOMPLETE") { // do your incomplete thing reject(); } else if (state === "ERROR") { // do your error thing reject(); } }); $A.enqueueAction(action); }); }
You would then call the helper method in the same way as usual
doInit : function( component, event, helper ) { helper.getData(); }
So, what are we doing here?
We have updated the helper function so that it now returns a Promise that is constructed with a new function that has two parameters 'resolve' and 'reject'. When the function is called, the Promise is returned and the function that we passed in is immediately executed.
When our function reaches its notional 'success' state (inside the 'state == "SUCCESS" section), we call the 'resolve' function that is passed in.
Similarly, when we get to an error condition, we call 'reject'.
In this simple case, you'll find it hard to see where 'resolve' and 'reject' are defined - because they're not. In this case the Promise will create an empty function for you and the Promise will essentially operate as if it wasn't there at all. The functionality hasn't changed.
So the obvious question is.. Why?
What does a Promise give you in such a situation?
Well, if all you are doing it calling a single function that has no dependant children, then nothing. But let's say that you wanted to call "getConfiguration", which called some Apex, and then *only once that was complete* you called "getData".
Without Promises, you'd have 2 obvious solutions:
- Call "getData" from the 'Success' path of "getConfiguration".
- Pass "getData" in as a callback on "getConfiguration" and call the callback in the 'Success' path of "getConfiguration"
That is - in the first we introduce an explicit dependancy between getConfiguration and getData. Ideally, this would not be expressed in getConfiguration, but rather in the doInit (or a helper function called by doInit). It is *that* function which decides that the dependancy is important.
The second solution *looks* much better (and is), but it's still not quite right. We now have an extra parameter on getConfiguration for the callback. We *should* also have another callback for the failure path - otherwise we are expressing that only success has a further dependancy, which is a partial leaking of knowledge.
Fulfilling your Promise - resolve and reject
When we introduce Promises, we introduce the notion of 'then'. That is, when we 'call' the Promise, we are able to state that something should happen on 'resolve' (success) or 'reject' (failure), and we do it from *outside* the called function.
Or, to put it another way, 'then' allows us to define the functions 'resolve' and 'reject' that will get passed into our Promise's function when it is constructed.
E.g.
We can pass a single function into 'then', and this will be the 'resolve' function that gets called on success.
doInit : function( component, event, helper ) { helper.getConfiguration( component ) .then( () => { helper.getData( component ) } ); }
Or, if we wanted a failure path that resulted in us calling 'helper.setError', we would pass a second function, which will become the 'reject' function.
doInit : function( component, event, helper ) { helper.getConfiguration( component ) .then( () => { helper.getData( component ) } , () => { helper.setError( component ) } ); }
Now, this solution isn't vastly different to passing the two functions directly into the helper function. E.g. like this:
doInit : function( component, event, helper ) { helper.getConfiguration( component , () => { helper.getData( component ) } , () => { helper.setError( component ) } ); }
And whilst I might say that I personally don't like the act of passing in the two callbacks directly into the function, personal dislike is probably not a good enough reason to use a new language feature in a business critical system.
So is there a better reason for doing it?
Promising everything, or just something
Thankfully, Promises are more than just a mechanism for callbacks, they are a generic mechanism for *guaranteeing* that 'settled' (fulfilled or rejected) Promises result in a specified behaviour occurring once certain states occur.
When using a simple Promise, we are simply saying that the behaviour should be that the 'resolve' or 'reject' functions get called. But that's not the only option
. For example, we also have:
Promise.all | Will 'resolve' only when *all* the passed in Promises resolve, and will 'reject' if and when *any* of the Promises reject. |
Promise.race | Will 'resolve' or 'reject' when the first Promise to respond comes back with a 'resolve' or 'reject'. |
How about having the component load with a 'loading spinner' that is only switched off when all three calls to Apex respond with success:
doInit : function( component, event, helper ) { Promise.all( [ helper.getDataOne( component ) , helper.getDataTwo( component ) , helper.getDataThree( component ) ] ) .then( () => { helper.setIsLoaded( component ) } ); }
Or even better - how about we call getConfiguration, then once that’s done we call each of the getData functions, and only when all three of those are finished do we set the flag:
doInit : function( component, event, helper ) { helper.getConfiguration( component ) .then( Promise.all( [ helper.getDataOne( component ) , helper.getDataTwo( component ) , helper.getDataThree( component ) ] ) .then( () => { helper.setIsLoaded( component ) } ) ); }
Now, just for a second, think about how you would do that without Promises...