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;
@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