Skip to content

Instantly share code, notes, and snippets.

@auser
Last active May 26, 2021 19:03
Show Gist options
  • Save auser/1d55aa3897f15d17caf21dc39b85b663 to your computer and use it in GitHub Desktop.
Save auser/1d55aa3897f15d17caf21dc39b85b663 to your computer and use it in GitHub Desktop.

ScriptCache + React + Google Api

The 3 scripts in here are separated for clarity. They are:

  • ScriptCache.js - The backbone of this method which asynchronously loads JavaScript <script> tags on a page. It will only load a single <script> tag on a page per-script tag declaration. If it's already loaded on a page, it calls the callback from the onLoad event immediately.

Sample usage:

this.scriptCache = cache({
  google: 'https://api.google.com/some/script.js'
});
  • GoogleApi.js is a script tag compiler. Essentially, this utility module builds a Google Script tag link allowing us to describe the pieces of the Google API we want to load inusing a JS object and letting it build the endpoint string.

Sample usage:

GoogleApi({
  apiKey: apiKey,
  libraries: ['places']
});
  • GoogleApiComponent.js - The React wrapper which is responsible for loading a component and passing through the window.google object after it's loaded on the page.

Sample usage:

const Container = React.createClass({
  render: function() {
    return <div>Google</div>;
  }
})
export default GoogleApiComponent({
  apiKey: __GAPI_KEY__
})(Container)
export const GoogleApi = function(opts) {
opts = opts || {}
const apiKey = opts.apiKey;
const libraries = opts.libraries || [];
const client = opts.client;
const URL = 'https://maps.googleapis.com/maps/api/js';
const googleVersion = '3.22';
let script = null;
let google = window.google = null;
let loading = false;
let channel = null;
let language = null;
let region = null;
let onLoadEvents = [];
const url = () => {
let url = URL;
let params = {
key: apiKey,
callback: 'CALLBACK_NAME',
libraries: libraries.join(','),
client: client,
v: googleVersion,
channel: channel,
language: language,
region: region
}
let paramStr = Object.keys(params)
.filter(k => !!params[k])
.map(k => `${k}=${params[k]}`).join('&');
return `${url}?${paramStr}`;
}
return url();
}
export default GoogleApi
import React, { PropTypes as T } from 'react'
import ReactDOM from 'react-dom'
import cache from 'utils/cache'
import GoogleApi from 'utils/GoogleApi'
const defaultMapConfig = {}
export const wrapper = (options) => (WrappedComponent) => {
const apiKey = options.apiKey;
const libraries = options.libraries || ['places'];
class Wrapper extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
loaded: false,
map: null,
google: null
}
}
componentDidMount() {
const refs = this.refs;
this.scriptCache.google.onLoad((err, tag) => {
const maps = window.google.maps;
const props = Object.assign({}, this.props, {
loaded: this.state.loaded
});
const mapRef = refs.map;
const node = ReactDOM.findDOMNode(mapRef);
let center = new maps.LatLng(this.props.lat, this.props.lng)
let mapConfig = Object.assign({}, defaultMapConfig, {
center, zoom: this.props.zoom
})
this.map = new maps.Map(node, mapConfig);
this.setState({
loaded: true,
map: this.map,
google: window.google
})
});
}
componentWillMount() {
this.scriptCache = cache({
google: GoogleApi({
apiKey: apiKey,
libraries: libraries
})
});
}
render() {
const props = Object.assign({}, this.props, {
loaded: this.state.loaded,
map: this.state.map,
google: this.state.google,
mapComponent: this.refs.map
})
return (
<div>
<WrappedComponent {...props} />
<div ref='map' />
</div>
)
}
}
return Wrapper;
}
export default wrapper;
let counter = 0;
let scriptMap = new Map();
export const ScriptCache = (function(global) {
return function ScriptCache (scripts) {
const Cache = {}
Cache._onLoad = function (key) {
return (cb) => {
let stored = scriptMap.get(key);
if (stored) {
stored.promise.then(() => {
stored.error ? cb(stored.error) : cb(null, stored)
})
} else {
// TODO:
}
}
}
Cache._scriptTag = (key, src) => {
if (!scriptMap.has(key)) {
let tag = document.createElement('script');
let promise = new Promise((resolve, reject) => {
let resolved = false,
errored = false,
body = document.getElementsByTagName('body')[0];
tag.type = 'text/javascript';
tag.async = false; // Load in order
const cbName = `loaderCB${counter++}${Date.now()}`;
let cb;
let handleResult = (state) => {
return (evt) => {
let stored = scriptMap.get(key);
if (state === 'loaded') {
stored.resolved = true;
resolve(src);
// stored.handlers.forEach(h => h.call(null, stored))
// stored.handlers = []
} else if (state === 'error') {
stored.errored = true;
// stored.handlers.forEach(h => h.call(null, stored))
// stored.handlers = [];
reject(evt)
}
cleanup();
}
}
const cleanup = () => {
if (global[cbName] && typeof global[cbName] === 'function') {
global[cbName] = null;
}
}
tag.onload = handleResult('loaded');
tag.onerror = handleResult('error')
tag.onreadystatechange = () => {
handleResult(tag.readyState)
}
// Pick off callback, if there is one
if (src.match(/callback=CALLBACK_NAME/)) {
src = src.replace(/(callback=)[^\&]+/, `$1${cbName}`)
cb = window[cbName] = tag.onload;
} else {
tag.addEventListener('load', tag.onload)
}
tag.addEventListener('error', tag.onerror);
tag.src = src;
body.appendChild(tag);
return tag;
});
let initialState = {
loaded: false,
error: false,
promise: promise,
tag
}
scriptMap.set(key, initialState);
}
return scriptMap.get(key);
}
Object.keys(scripts).forEach(function(key) {
const script = scripts[key];
Cache[key] = {
tag: Cache._scriptTag(key, script),
onLoad: Cache._onLoad(key)
}
})
return Cache;
}
})(window)
export default ScriptCache;
@zjaml
Copy link

zjaml commented Dec 7, 2016

Hey auser, thanks for sharing this gist! I've been trying to find a way to load external scripts from the dependent React component for a long time, this is definitely the most elegant way to do it!

Thank you so much!

@joeida
Copy link

joeida commented Dec 13, 2016

@auser
Hi, I was trying to make this code work manually in my code, but I get an error stating that the ./utils/cache file is missing. I checked the code and don't see the cache file. Do I need to create some sort of manual cache file for this to work?

Thanks in advance!

@dtipson
Copy link

dtipson commented Dec 14, 2016

let google = window.google = null; is for some reason, deleting google from the global object when the component is reloaded, which afaict, shouldn't happen based on how ScriptCache works, but does anyhow.

@agrcrobles
Copy link

it is happening the same to me let google = window.google = null; is deleting google from window when reloading, any thoughts?

@pkrawc
Copy link

pkrawc commented Mar 27, 2017

Should just be conditional. if (window.google === 'undefined') let google = window.google = null

@akshaynanavati
Copy link

Does this work with multiple maps on the same page?

@tomhalley
Copy link

The bit in ScriptCache.js on line 68 where you're replacing CALLBACK_NAME with the callback name is also stripping off libraries off of the end of the URL. For the time being I've fixed it by putting the callback as the last part of the Url in GoogleApi.js, but if anyone can come up with a better fix please let me know!

@nazreen
Copy link

nazreen commented Jun 30, 2017

@joeida, not sure if still relevant to you, but I was scratching my head over the same thing, until I realised the pattern in the names. cache and GoogleApi are referring to the other 2 scripts. just change the import file location and name to refer to those scripts, and it will work.

@Rolandisimo
Copy link

Am I the only one that questions these imports?

import cache from 'utils/cache'
import GoogleApi from 'utils/GoogleApi'

Where are they coming from?

@pmillssf
Copy link

@Rolandisimo I think they just copied files from their github project into the gist, I believe 'utils/cache' is referring to ScriptCache.js and utilis/GoogleApi is referring to the GoogleApi.js file. They didn't update the file structure of linking these files together after they pulled it out of a project, so it reads as an import from a utils folder.

@Serzhs
Copy link

Serzhs commented Dec 22, 2017

Really bad example and explanation how to import and use these functions

@stramel
Copy link

stramel commented Dec 6, 2018

Hope this helps, just used memoization for caching,

import { memoize } from 'lodash-es'

/**
 * WARNING: Use this method directly to avoid memoization cache
 * @param {string} src - URL of script to lazy-load
 */
export const load = src =>
  new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.type = 'text/javascript'
    script.charset = 'utf-8'
    script.async = false

    // Handlers
    script.onload = resolve
    script.onerror = reject
    script.onreadystatechange = () => {
      if (script.readyState === 'loaded') {
        resolve()
      } else {
        reject()
      }
    }

    script.src = src
    document.body.appendChild(script)
  })

const cachedLoad = memoize(load)

/**
 * Use this to handle caching of script imports
 * @param {string} src - URL of script to lazy-load
 * @param {boolean} [force] - Skip cached load
 */
export default (src, force) => {
  if (force) {
    return load(src)
  }
  return cachedLoad(src)
}

@tristanheilman
Copy link

Could this be used in react-native?

@Limpuls
Copy link

Limpuls commented Mar 11, 2020

Not really sure why do you need a callback handler if we are loading this dynamically, so we can just write our own callback straight in the onload handler, which will run the code only when script is loaded, instead of passing a callback in the src of the script and catching it with regex.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment