Don't Render, I'm Still Loading the Script...
Nowadays we normally load script tags asynchronously and try not to block other things as much as possible. A lot of APIs are designed to be innately asynchronous. What can we do to make sure certain things happen after certain script is executed? And in particular, when the callback isn't explicitly given as a Promise
?
Problem
Today, well, not today, recently, I've got this problem:
The gapi works this way:
- you include the gapi bundle
- the bundle has no other job but to load some other bundle (one that you will use)
- after the module you need (
auth
in my case) is loaded, you still need to initialize an instance with some customized data (client id, as an example), which is yet another asynchronous call - then you have access to that instance, which in turn has the instance methods you will use
From Google's docs, here's how you load the script for the API:
<script
src="https://apis.google.com/js/platform.js?onload=init"
async
defer
></script>
What this does is it will load the said script asynchronously, meaning it will not block other resources from loading or executing, and defer
means the script is meant to be executed after the document has been parsed (MDN docs).
The ?onload=init
search query means that it will call the init
function you bind to window
once the gapi
bundle is ready. So normally you write gapi.load
in an init
function:
function init() {
gapi.load('auth2', function() {
/* Ready. Make a call to gapi.auth2.init or some other API */
});
}
The instance initialization (step 3 mentioned above) is another async call that returns a "thenable" type.
This workflow probably works desirably for most sites. Except in a particular project I'm working on recently, I want to block something from happening until the whole authentication instance is ready. We're talking about login, and in particular, I need the authentication instance to be instantiated as early as possible, and whichever page that ever inquires whether the user is logged in or not will hold its first execution until the auth instance is ready.
There are other reasons why I cannot let that particular part of my app go through multiple render passes but to wait on its first render. In summary, I have very tight requirement on how this workflow must happen once and each part must wait properly.
So how to waiiiiiiit
The script
tag
- put it in
head
- remove
async
,defer
, make it blocking 🙄
Waiting for "thenable" object
window.gapi.auth2
.init({
client_id: 'client-id',
})
.then(auth => {
auth.signIn(); // or other instance methods
});
If something is "thenable", i.e., async functions and promises, we can await
:
await window.gapi.auth2.init();
and it will properly wait.
Turning a load
callback into a promise
What got me a lot of trouble was this line
gapi.load('auth2', callback);
I need to provide that callback
, which will happen once auth2
is loaded. But I have no control over when that happens and finishes. So how 🤷🏻♀️
I was very stuck until I saw an one of the examples from Google, where you can create and return a promise, and pass resolve
as a callback:
var loadGapiClient = new Promise(function(resolve, reject) {
gapi.load('clent:auth2', resolve);
});
MDN docs also gave another simpler example but somehow it never occured to me that I could create a Promise
and pass its resolve
as callback to gapi.load
, which effectively transfers the callback
into something "thenable", until I saw Google's example in a closer context.
const load = new Promise(function(resolve, reject) {
gapi.load('auth2', resolve);
});
This turns the original candidate for callback
, we don't have hold of its start and end of execution, into "thenable" type. Note how even callback
may not even be asynchronous to begin with, because it was previously at a "callback" position, we lose control of that workflow.
After wrapping it in a Promise
, we still don't have hold of when it happens, but we get better control of our entire workflow because it now expresses that certain things should happen in sequence.
load.then(() => {
window.gapi.auth2
.init({
client_id: 'client-id',
})
.then(auth => {
auth.signIn(); // or other instance methods
});
});
But I was still pretty stuck. I now kind of have this process that happens in sequence. How do I make sure that all my related function will hang on this workflow, in case it's not completed?
So I brought this to my senpai who solved the mystery by telling me whenver you have a Promise you can return it and put it into an async
function, then you can await
that async
function that will make it kinda synchronous.
It's probably not the exact words but that's a same kind of words my first college math professor said
you can do an integration by parts when you have two components one multiplied by the derivatives of the other, added by the derivative of itself multiplied by the other
..., something like that. But let's keep math to just that extent.
So eventually my implementation looked like this:
async function loadAuthInstance() {
const load = new Promise(function(resolve, reject) {
gapi.load('auth2', resolve);
});
return load.then(async () => {
return await window.gapi.auth2
.init({
client_id: 'client-id',
})
.then(authInstance => {
window.authInstance = authInstance;
});
});
}
And other parts that depend on window.authInstance
can now properly wait:
function login() {
if (!window.authInstance) {
await loadAuthInstance();
}
window.authInstance.login();
}
On a side note, if you keep returning the await
in the then()
callbacks, you're "chaining async functions". This is a very hipster action and there are already a whole world of people talking about it.
Christmas wishes
- include more real world examples in guides
- summarize use cases in sensible ways
- get unafraid of
Promise
,async
andawait
Speaking of takeaways, this line clicks for me:
Synchronous functions return values, async ones do not and instead invoke callbacks