Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

agrcrobles commented Mar 8, 2017

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

@pkrawc

This comment has been minimized.

Copy link

pkrawc commented Mar 27, 2017

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

@akshaynanavati

This comment has been minimized.

Copy link

akshaynanavati commented Apr 2, 2017

Does this work with multiple maps on the same page?

@tomhalley

This comment has been minimized.

Copy link

tomhalley commented Apr 5, 2017

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

Rolandisimo commented Jul 29, 2017

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

This comment has been minimized.

Copy link

pmillssf commented Aug 20, 2017

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

@Espumisan

This comment has been minimized.

Copy link

Espumisan commented Dec 22, 2017

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

@stramel

This comment has been minimized.

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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.