6

My index.html

<!DOCTYPE html>
<html lang="en">
    <head>

        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="google-signin-client_id" content= "my_client_id.apps.googleusercontent.com">

        <meta name="google-signin-scope" content="profile email">
        <script src="https://apis.google.com/js/client:platform.js?onload=start" async defer></script>
        <script>
            function start() {
                console.log('script running')
                gapi.load('auth2', function() {
                    auth2 = gapi.auth2.init({
                        client_id: 'my_client_id.apps.googleusercontent.com',
                        scope: 'profile email'
                    });
                });
            }
        </script>
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>


  </body>
</html>

In the start() function I print to the console to see when it's running.

When I load the page, every so often start() will load after the react components.

Login.js

    componentDidMount() {
        console.log(gapi.auth2.getAuthInstance())
    }

In the debugger you can see that the script is loading after the component:

enter image description here

If I refresh the page a few times, it works fine. But sometimes it works, sometimes it doesn't.

Why?

Matt Fletcher
  • 8,182
  • 8
  • 41
  • 60
Morgan Allen
  • 3,291
  • 8
  • 62
  • 86
  • You ask _why?_ but in all reality, I assume there is no why. It's just a race condition. Sometimes the code blocks are processed in one order, sometimes another. How are you calling `start()` and how are you initialising react components? Are they being done synchronously? – Matt Fletcher Nov 21 '17 at 20:02
  • start() is being called in the HTML file `"https://apis.google.com/js/client:platform.js?onload=start"` – Morgan Allen Nov 21 '17 at 20:03
  • Ah yes, so that's async. If you remove `async` and `defer` from that tag, does it work? I'd imagine not because of the way google apis is forcing async loading – Matt Fletcher Nov 21 '17 at 20:04
  • And even if it does start working as far as the console.log is concerned, the actual google API will probably still suffer the same race condition. Really you'll want to initialise the react components inside the callback of `gapi.auth2.init()`, or make sure that react calls that before trying to access any of its info – Matt Fletcher Nov 21 '17 at 20:05
  • is that what async defer does? motherfcker should have figured that out one. I've been killing myself over this. – Morgan Allen Nov 21 '17 at 20:06
  • `Really you'll want to initialise the react components inside the callback of gapi.auth2.init()` Not sure how I'd do that, but helpful to think about how to make this work :-) – Morgan Allen Nov 21 '17 at 20:07
  • hm...I took out async defer and now the script doesn't run at all.... – Morgan Allen Nov 21 '17 at 20:09
  • Yeah it probably wouldn't do. Take off the `async defer` and then call `start()` manually, or just remove the function wrapper entirely, which should do the same as it's currently doing, but will block the thread 'cause it's synchronous. But I feel the solution is to rethink how you're adding that script, and put it more inside the "reactoverse". You'd also have to make sure both the googleAPI and react code is running synchronously, not just one. – Matt Fletcher Nov 21 '17 at 20:12
  • yea, I don't understand how other people have such an easy time with this. Hah, this is the most frustrating annoying thing in the world. – Morgan Allen Nov 21 '17 at 20:14
  • Trust me, no-one has an easy time with it :P Have a look at this: https://developers.google.com/maps/documentation/javascript/examples/map-sync - I might also add an answer – Matt Fletcher Nov 21 '17 at 20:16
  • 1
    Might want to obfuscate the `client_id` – mhatch Nov 21 '17 at 20:17
  • its embedded in the HTML - https://developers.google.com/identity/sign-in/web/sign-in so it seems that its ok that its publicly shared? its in a meta tag of my HTML file so seems like anyone can see it – Morgan Allen Nov 21 '17 at 20:18
  • I just mean for SO... Other applications can hijack it unless you restrict the domain in google console. – mhatch Nov 21 '17 at 20:19
  • yea, it is :-) localhost :D – Morgan Allen Nov 21 '17 at 20:19

2 Answers2

7

I think the best way to load scripts in react is with a container component. It's a pretty simple component and it allows to you write the logic for importing the script in a component rather than your index.html. You are also going to want to make sure you don't include the script more than once by calling loadScript after a check in componentDidMount.

This is adapted from: https://www.fullstackreact.com/articles/how-to-write-a-google-maps-react-component/

Something like this. . .

  componentDidMount() {
    if (!window.google) {
      this.loadMapScript();
    }
    else if (!window.google.maps) {
      this.loadMapScript();
    }
    else {
      this.setState({ apiLoaded: true })
    }
  }

  loadMapScript() {
    // Load the google maps api script when the component is mounted.

    loadScript('https://maps.googleapis.com/maps/api/js?key=YOUR_KEY')
      .then((script) => {
        // Grab the script object in case it is ever needed.
        this.mapScript = script;
        this.setState({ apiLoaded: true });
      })
      .catch((err: Error) => {
        console.error(err.message);
      });
  }

  render() {
    return (
      <div className={this.props.className}>
        {this.state.apiLoaded ? (
          <Map
            zoom={10}
            position={{ lat: 43.0795, lng: -75.7507 }}
          />
        ) : (
          <LoadingCircle />
        )}
      </div>
    );
  }

and then in a separate file:

const loadScript = (url) => new Promise((resolve, reject) => {
  let ready = false;
  if (!document) {
    reject(new Error('Document was not defined'));
  }
  const tag = document.getElementsByTagName('script')[0];
  const script = document.createElement('script');

  script.type = 'text/javascript';
  script.src = url;
  script.async = true;
  script.onreadystatechange = () => {
    if (!ready && (!this.readyState || this.readyState === 'complete')) {
      ready = true;
      resolve(script);
    }
  };
  script.onload = script.onreadystatechange;

  script.onerror = (msg) => {
    console.log(msg);
    reject(new Error('Error loading script.'));
  };

  script.onabort = (msg) => {
    console.log(msg);
    reject(new Error('Script loading aboirted.'));
  };

  if (tag.parentNode != null) {
    tag.parentNode.insertBefore(script, tag);
  }
});


export default loadScript;

I know it's a lot but when I was first doing this it was so relieving when I found out there was a (fairly) simple way of including any script in any react component.

Edit: Some of this I just copy pasted but if you aren't using create-react-app you will probably have to replace some of the ES6 syntax.

Dakota
  • 1,254
  • 8
  • 16
3

My suggestions:

Change your google API script tag to be this, where you remove async and defer

<script src="https://apis.google.com/js/client:platform.js"></script>

Get rid of your start function, which will now run the console.log fine and dandy, but the second bit of code will cause the same issue as it will also be running asynchronously.

Modify your react code, so that the componentWillMount calls the contents of that function instead:

componentWillMount() {
  gapi.load('auth2', () => {
    auth2 = gapi.auth2.init({
      client_id: 'urhaxorid.apps.googleusercontent.com',
      scope: 'profile email',
      onLoad: () => {
        this.setState({ mapLoaded: true });
      }
    });
  });
}

componentDidMount() {
  if (this.state.mapLoaded) {
    console.log(gapi.auth2.getAuthInstance());
  }
}

Please bear in mind that I don't know what the onLoad is for google apis, and I am not 100% sure how to best do the setState stuff, but it may be a starting block.

Matt Fletcher
  • 8,182
  • 8
  • 41
  • 60