rice-apps/match

GAPI client sometimes not initialized correctly

Closed this issue · 4 comments

So.... the GAPI client is used for the Google OAuth sign in workflow, and for all the Google Sheets API calls.

It works like so:

In public/index.html, we initially load the Google code through a CDN:

match/public/index.html

Lines 33 to 39 in 6fc1185

<script src="https://apis.google.com/js/platform.js?onload=triggerGoogleLoaded" async defer ></script>
<script type="text/javascript">
// When google api is loaded, we want to dispatch a new event in the browser window
function triggerGoogleLoaded() {
window.dispatchEvent(new Event('google-loaded'));
}
</script>

The embedded js script above should dispatch the event "google-loaded" every time the google platform code is finished loading into our app.

Then in Header.js component, what I've done is have a useEffect(func, []) <- empty list as second arg so it only fires once, and the function just creates an event listener for "google-loaded".

// Triggers google to initialize the client
useEffect(() => {
console.log("creating event listener for google-loaded")
window.addEventListener("google-loaded", () => handleClientLoad(authenticationCallback));
}, []);

If you see, the function calls handleClientLoad, which is in util/gapi.js:

match/src/util/gapi.js

Lines 19 to 46 in 6fc1185

/**
* On load, called to load the auth2 library and API client library.
*/
export function handleClientLoad(callbackFunction) {
window.gapi.load('client:auth2', () => initClient(callbackFunction));
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient(callbackFunction) {
window.gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
console.log("Initialized gapi client correctly.")
// Listen for sign-in state changes.
window.gapi.auth2.getAuthInstance().isSignedIn.listen((status) => updateSigninStatus(status, callbackFunction));
// Handle the initial sign-in state.
updateSigninStatus(window.gapi.auth2.getAuthInstance().isSignedIn.get(), callbackFunction);
}, function (error) {
alert("Error loading in CSV:" + JSON.stringify(error, null, 2));
});
}

This handleClientLoad function does the initialization by triggering initClient.

However, see in handleAuthClick, that is where I catch if window.gapi is not loaded, then I call an alert. Basically if the script in html did not load in correctly, then window.gapi will not be initialized.

You should be asking: What's the point of the useEffect? And why don't I just call window.addEventListener from outside the React component?

The reason is, to install a callback function (take a look at the function authenticationCallback) that can modify Recoil state, we must have access to those React hooks (we want to add the user info to Recoil state once user logs in), which means we must call handleClientLoad from within the scope of a React component.

I am pretty sure the issue is with this weird work-around to install this callback func. I suspect that sometimes the useEffect call is not triggered before the google cdn in index.html is finished loading, so that the handleClientLoad doesn't get triggered, and window.gapi is not initialized.

There is pleeenty of documentation on how to use their google-api-javascript-client.

Google something like "google api javascript client initialization" or sth

Some potential solutions here:
google/google-api-javascript-client#399

Should be fixed!
Just added a check to see if the API has already loaded before creating the listener.

625a199
2aa15a7

So the issue was that it would create an event listener after the gapi client has been initialized?

Yeah, not after the client had been initialized, but after the script finished loading. So the 'google-loaded' event was occurring before the event listener was created, since the file can load pretty quickly.

To test this, I added a short delay before creating the listener with
window.addEventListener("google-loaded", () => handleClientLoad(authenticationCallback));
and the issue was reproduced every time.

So, my fix was to check if the script has already loaded (and therefore the "google-loaded" event has already happened) before creating the listener. If the script has already loaded, we can just call handleClientLoad directly rather than waiting for the event:

// Check if api is already loaded
if (window.gapi) {
    handleClientLoad(authenticationCallback);
} else { 
    // Not already loaded; create event listener
    console.log("creating event listener for google-loaded")
    window.addEventListener("google-loaded", () => handleClientLoad(authenticationCallback));
}

Great work!!