Search code examples
javascriptreactjsgoogle-api-js-client

Setting ReactJS state from 3rd party JS file onload


I'm basically trying to do this from within a componentWillMount() function of a React component class.

    const script = document.createElement("script");

    script.src = "https://apis.google.com/js/client.js";
    script.async = true;
    script.onload = function() {
      gapi.client.setApiKey(API_KEY);
      gapi.client.load('youtube', 'v3');
      this.setState({ gapiReady: true }); 
      // ^ Doesn't work because `this` refers to the script rather than the component itself.
    };

    document.body.appendChild(script);

Entire component pasted here for completeness:

import React, { Component } from 'react';
import VideoPlayer from '../../components/VideoPlayer/VideoPlayer';
import VideoInfo from '../../components/VideoInfo/VideoInfo';
import SearchBox from '../../components/SearchBox/SearchBox';
import './App.css';

class App extends Component {
  constructor(props, context) {
    super(props, context);
    this.state = {};
  };

  componentWillMount() {
    const script = document.createElement("script");

    script.src = "https://apis.google.com/js/client.js";
    script.async = true;
    script.onload = function() {
      gapi.client.setApiKey(API_KEY);
      gapi.client.load('youtube', 'v3');
      this.setState({ gapiReady: true }); // Doesn't work because `this` refers to the script rather than the component
    };

    document.body.appendChild(script);
  };

  render() {
    if (this.state.gapiReady) {
      return (
        <div className="wrap">
          <div className="sidebar">
            <SearchBox />
            <VideoInfo videoId="vkRDOcma9Qk" />
          </div>
          <div className="content">
            <VideoPlayer />
          </div>
        </div>
      );
    } else {
      return (
        <div>Loading...</div>
      )
    }
  };
}

export default App;

My goal is to only render the VideoInfo and VideoPlayer components in the render() function when the script has fully loaded, because otherwise code on this components will fail since they rely on the script having been loaded.

Am I going about this the wrong way?


Solution

  • First of all, you should do stuff like this is componentDidMount so that the code can still run even in the case that it does not have access to the document object. This doesn't matter a whole lot if you're not doing server-side rendering, but it's a common pattern in React.

    To fix the problem with this, you can use an arrow function to define script.onload. Functions defined with the arrow function do not create a new context, so this remains the same as the scope the function is defined in.

    TLDR replace your componentWillMount method with this:

    componentDidMount () {
      const script = document.createElement("script")
    
      script.src = "https://apis.google.com/js/client.js"
      script.async = true
    
      script.onload = () => {
        gapi.client.setApiKey(API_KEY)
        gapi.client.load('youtube', 'v3')
        this.setState({ gapiReady: true })
      }
    
      document.body.appendChild(script)
    }