Giter Site home page Giter Site logo

react-async-script's Introduction

React Async Script Loader

Build Status npm version npm downloads

A React HOC for loading 3rd party scripts asynchronously. This HOC allows you to wrap a component that needs 3rd party resources, like reCAPTCHA or Google Maps, and have them load the script asynchronously.

Usage

Async Script HOC api

makeAsyncScriptLoader(getScriptUrl, options)(Component)

  • Component: The Component to wrap.
  • getScriptUrl: string or function that returns the full URL of the script tag.
  • options (optional):
    • attributes: object : If the script needs attributes (such as data- attributes), then provide them as key/value pairs of strings and they will be added to the generated script tag.
    • callbackName: string : If the script needs to call a global function when finished loading (for example: recaptcha/api.js?onload=callbackName). Please provide the callback name here and it will be autoregistered on window for you.
    • globalName: string : Can provide the name of the global that the script attaches to window. Async-script will pass this as a prop to the wrapped component. (props[globalName] = window[globalName])
    • removeOnUnmount: boolean default=false : If set to true removes the script tag when component unmounts.
    • scriptId: string : If set, it adds the following id on the script tag.

HOC Component props

const AsyncScriptComponent = makeAsyncScriptLoader(URL)(Component);
// ---
<AsyncScriptComponent asyncScriptOnLoad={callAfterScriptLoads} />
  • asyncScriptOnLoad: function : called after script finishes loading. using script.onload

Ref and forwardRef

react-async-script uses react's forwardRef method to pass along the ref applied to the wrapped component.

If you pass a ref prop you'll have access to your wrapped components instance. See the tests for detailed example.

Simple Example:

const AsyncHoc = makeAsyncScriptLoader(URL)(ComponentNeedsScript);

class DisplayComponent extends React.Component {
  constructor(props) {
    super(props);
    this._internalRef = React.createRef();
  }
  componentDidMount() {
    console.log("ComponentNeedsScript's Instance -", this._internalRef.current);
  }
  render() { return (<AsyncHoc ref={this._internalRef} />)}
}
Notes on Requirements

At least [email protected] is required due to forwardRef usage internally.

Example

See https://github.com/dozoisch/react-google-recaptcha

// recaptcha.js
export class ReCAPTCHA extends React.Component {
  componentDidUpdate(prevProps) {
    // recaptcha has loaded via async script
    if (!prevProps.grecaptcha && this.props.grecaptcha) {
      this.props.grecaptcha.render(this._container)
    }
  }
  render() { return (
    <div ref={(r) => this._container = r} />)
  }
}

// recaptcha-wrapper.js
import makeAsyncScriptLoader from "react-async-script";
import { ReCAPTCHA } from "./recaptcha";

const callbackName = "onloadcallback";
const URL = `https://www.google.com/recaptcha/api.js?onload=${callbackName}&render=explicit`;
// the name of the global that recaptcha/api.js sets on window ie: window.grecaptcha
const globalName = "grecaptcha";

export default makeAsyncScriptLoader(URL, {
  callbackName: callbackName,
  globalName: globalName,
})(ReCAPTCHA);

// main.js
import ReCAPTCHAWrapper from "./recaptcha-wrapper.js"

const onLoad = () => console.log("script loaded")

React.render(
  <ReCAPTCHAWrapper asyncScriptOnLoad={onLoad} />,
  document.body
);

react-async-script's People

Contributors

anajavi avatar dependabot[bot] avatar dozoisch avatar hartzis avatar javidjamae avatar jonne avatar kevincarrogan avatar kntsoriano avatar lemieux avatar mikehazell avatar newyork-anthonyng avatar voutilad avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

react-async-script's Issues

Intermittent "Script is not loaded" Error when using callbackName

Hey there,

We're getting this error intermittently when trying to load the GoogleMaps Api.

It looks like what's happening is that asyncScriptLoaderTriggerOnScriptLoaded is being called before the script onLoad event fires. Only happens about once in 5 refreshes.

I have a workaround that feels kind of hacky. It just ignores mapEntry.loaded if callbackName is defined.

      asyncScriptLoaderTriggerOnScriptLoaded() {
        let mapEntry = SCRIPT_MAP.get(scriptURL);
        const { callbackName } = options;
        if (!mapEntry || !mapEntry.loaded && !callbackName) {
          throw new Error("Script is not loaded.");
        }

I'd be happy to help out with this. Not sure what the right direction to go is though.

How do you pass params to the URL?

Hi,

In the example shown, the URL is known upfront and hardcoded. Is it possible to pass in props into the wrapper that helps determine what the URL is and then inject that URL being used?

In my use case, bing maps api, I need to add in query params to set the Language or local.

example of what i'm looking for.

<WrapperComponent urlProps={} {… componentProps} />

Then in the WrapperComponent it takes the url props and builds the URL that is injected into the default.

export default AsyncScriptLoader(BingMapWidget, URL, {
callbackName: callbackName,
globalName: globalName,
});

However, its not clear to me if it's possible to expose the wrapper to take in those params so then i can run a method to compute the URL and spit it back to the AsyncScriptLoader

Also i notice that you are attaching to the global scope. What happens if you launch two instances of the wrapped components. Would there be issues since they will share the same callback?

Thanks,
Derek

Any chance you could post a Stripe example?

I'm a bit confused on the examples.

I created a Stripe.jsx file:

import React, { Component } from 'react'
import makeAsyncScriptLoader from 'react-async-script'

class Stripe extends Component {
  constructor () {
    super()
    this.state = {
      message: 'INIT...'
    }
  }
  stripeJSLoaded (msg) {
    console.info('loaded.')
    this.setState({ message: 'Loaded.' })
  }
  render () {
    return (
      <div>{this.state.message}</div>
    )
  }
}

const callbackName = 'stripeJSLoaded'
const URL = 'https://js.stripe.com/v3/'
const globalName = 'stripe'

export default makeAsyncScriptLoader(Stripe, URL, {
  callbackName,
  globalName
})

Within the App.jsx I'm loading it:

import Stripe from './Stripe'
...
  onLoad () {
    console.log('script loaded')
  }
...

and rendering it:

<Stripe asyncScriptOnLoad={ this.onLoad } />

I get 'INIT...' on the screen, but nothing in the console or elsewhere.

At this point I'm just trying to get ANYTHING to load and then change some status to let me know it did it, let alone using that script.

Help?

Error handling

Hello. Is there any way to know if there's an error while loading script?

Script loads but neither onload function fires

I might be doing something wrong but I am not sure. Spend a couple of hours trying to figure it out and for the love of god I cannot get the onload function to fire. The script loads because i see it on the rendered page.

import ReactDOM from 'react-dom'
import makeAsyncScriptLoader from "react-async-script";
import Affdex from "./affdex";

const callbackName = 'checkAffdex';
const URL = `<OMITTED>?onLoad=${callbackName}`;
const globalName = 'affdex';

let WrappedAffdex = makeAsyncScriptLoader(Affdex, URL, {
    callbackName: callbackName,
    globalName: globalName
});

function onLoad() {
    console.log('Loaded');
}

const Affect = (elem) => {
    ReactDOM.render(<WrappedAffdex asyncScriptOnload={onLoad}/>, elem);
}

** Affdex.js ** 

import React, { Component } from 'react';
import ProgressBar from './helpers/progress-bar';
import PropTypes from 'prop-types';

class Affdex extends Component {
    constructor(props) {
        console.log(props);
        super(props);
        this.state = {affdexLoaded: false};
        this.renderAffdexElements = this.renderAffdexElements.bind(this);
        this.checkAffdex = this.checkAffdex.bind(this);
    }

    checkAffdex(){
        console.log('affect loaded');
        this.setState({affdexLoaded: true});
    }

    componentDidUpdate() {
        console.log('component updated');
    }

    componentDidMount() {
        // this.explicitRender();
        console.log('component mounted');
    }

    renderAffdexElements() {
        return (
            <div className="col s12 red">
                <div className="affdex-elements"></div>
                <button id="start" onClick={() => {/*this.onStart*/ console.log('clicked start')}}>Start</button>
                <button id="stop" onClick={() => {/*this.onStop*/ console.log('clicked stop')}}>Stop</button>
                <button id="reset" onClick={() => {/*this.onReset*/ console.log('clicked reset')}}>Reset</button>
            </div>
        );
    }


    render() {
        let toRender = this.state.affdexLoaded ? this.renderAffdexElements() : <ProgressBar col={12}/>;
        return (
            this.renderAffdexElements()
        );
    }
}

export default Affdex;

Upgrade peer dependency for babel 6 and 7

Your peer dependcy for babel-runtime isn't working with newer versions of babel:

warning "react-google-recaptcha > [email protected]" has unmet peer dependency "babel-runtime@>=5.8.0".

But I clearly have plenty of babel modules and package works fine:

$ yarn list | grep babel | grep run

├─ @babel/[email protected]
├─ @babel/[email protected]
│  ├─ babel-runtime@^6.26.0
│  ├─ babel-runtime@^6.22.0
│  └─ babel-runtime@^6.22.0
│  │  ├─ babel-runtime@^6.26.0
│  ├─ babel-runtime@^6.26.0
├─ [email protected]
│  ├─ babel-runtime@^6.26.0
│  ├─ babel-runtime@^6.26.0
│  ├─ babel-runtime@^6.26.0
│  │  ├─ babel-runtime@^6.26.0
│  │  ├─ babel-runtime@^6.26.0
│  ├─ @babel/[email protected]
│  ├─ @babel/[email protected]

Looks like you could do "babel-runtime": "^5.8.0 || ^6.0.0"

Unfortunately, I'm not sure how to deal with babel 7 since it's now namespaced at @babel/runtime. Maybe they've made a bridge package for it and this would work:

"babel-runtime": "^5.8.0 || ^6.0.0 || ^7.0.0"

No close event

I'm missing a close event handler/callback. This close event should fire when user click outside the challenge box and challenge box is closing.

Wrong window context when using an iFrame

Hi !
I am using the react-google-recaptcha lib as well as react-frame-component in my project in order to create a widget, and I found out that react-google-recaptcha is not usable inside the iframe created by react-frame-component.
After a bit of investigation and discussion (see here) it seems that the issue comes from react-async-script, which does not enable us to choose the context (window / document) to use in order to load the script.

Here is a lighter version of my code, to get the idea of how do I use the different libs I mentioned :

import React from 'react';
import ReCAPTCHA from 'react-google-recaptcha';
import WidgetFrame from 'react-frame-component';
import { ConnectedRouter } from 'react-router-redux';
import { Route } from 'react-router';
import { Provider } from 'react-redux';

const store = createStore(...);

const SetupView extends React.Component {
  constructor (props) {
   super(props);
   this.handleReCaptchaChanged = () => {
       // stuff
   };
  } 

  render () {
    return (
        <div>
          { ... }
          <ReCAPTCHA
            sitekey={secureKey.key}
            onChange={this.handleReCaptchaChanged}
          />
        </div>
    );
}
}

const Layout = ({ children }) => {
// __IFRAME_CONTENT__ is replaced during the browserify build operation to match a one-line version of a HTML template, which embed custom iframe-scoped style
    return (
       <WidgetFrame
           initialContent='__IFRAME_CONTENT__'
           id='app-iframe'
        >
          { ... }
          {children}
    </WidgetFrame>
    );
};

const Router = ({ history }) => {
    return (
        <ConnectedRouter history={history}>
          <Layout>
            <Route exact path='/' component={...} />
            { ... }
            <Route path='/setup' component={SetupView} />
          </Layout>
        </ConnectedRouter>
    );
};

// `app` is the button my sdk creates and mounts into the client's webpage
ReactDOM.render(
  <Provider store={store}>
    <Router history={history} />
  </Provider>,
  app
);

Can you please take a look to confirm that we did identify the issue correctly, and give us more insight on whether or not the required window/document configuration option will be implemented ?

Thanks for your help :)

Feature request: Support script tag attributes

I'm integrating with a 3rd party library (OptinMonster) that has a script tag which uses data- attributes to customize the script for different customers.

Here is the structure of their script tag:

<script type="text/javascript" src="https://a.opmnstr.com/app/js/api.min.js" data-account="12345" data-user="54321" async></script>

I looked through the react-async-script code and it seems like there is currently no way to provide any optional attributes to the script tag that is generated.

It would be great if we could provide an optional object of attributes that should be injected into the script tag when generated.

Something like this:

const URL = 'https://a.opmnstr.com/app/js/api.min.js'
const options = {
  tagAttributes: {
    'data-account': '12345',
    'data-user': '54321',
  },
}
makeAsyncScriptLoader(URL, options)(Component)

Setting component to rerender when script loaded

Currently I have a component: QuickAdd that needs the google autocomplete api to run

However, while the component renders, it doesn't seem like the autocomplete has finished loading. If i go to another component and then return to the QuickAdd component then it works, which makes me think it needs to rerender after the script is loaded.

The globalName is always passed as a prop and but is always undefined. What other listener can i use for this?

const callbackName = "onloadcallback"; const globalName = "quickAddLoader"; const url = https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places`;
const AsyncHoc = makeAsyncScriptLoader(url, {globalName: globalName})(QuickAdd);
export default AsyncHoc; `

HOC and v1.0.0 release

  • propose turning this into a HOC pattern ✅
    • (options) => (WrapComponent) => WrappedWithAsyncScript
    • it's so close to being this pattern all ready and is more conducive with community standards. ie. recompose
    • example from this comment and his re-write in fork
  • propose cutting a v1.0.0 with the above HOC pattern and current options
    • react-async-script has been a solid choice in the react community for years with a solid API

TODO:

  • PR for "standard" HOC pattern
  • drop dev support for NODE 4/5/6/7. No longer LTS. 6 is only maintenance.
  • drop option exposeFuncs and use hoist-non-react-statics
  • use forwardRef - work started
    • sets peer dep to "react": ">= 16.3.0"
    • #37
  • update readme with migration to 1.0.0 section
  • test release candidate with react-google-recaptcha - dozoisch/react-google-recaptcha#91

Clean up bundled polyfills

Hello!

While attempting to remove all polyfills for a webpack build and instead use polyfill.io I found that react-google-recaptcha uses this repo which uses babel-runtime.

babel-runtime creates hard polyfill depdencies during transpilation. Example:

var _getIterator2 = require("babel-runtime/core-js/get-iterator");
var _map = require("babel-runtime/core-js/map");

You can see the transpiled code that require's the polyfills here:
https://unpkg.com/[email protected]/lib/async-script-loader.js

These polyfills bundled with this library add 21kbparsed and 7.5kb gzipped to bundles and can not be tree-shaken or removed if the same polyfills are already being introduced in the bundle.

Two possible solutions:

  1. clean up/dumb down source code to use different solutions. ie Map -> just plain object?
  2. just transpile source code to es5 and not polyfill. Update the readme to make note of which features the user should polyfill if needed

I'm in favor of the second option and can make a PR to do it 😸 I'm in the boat that thinks packages should not polyfill, but instead let their users polyfill as needed based on their own browser support.

Cheers.

react_async_script_1.default is not a function

hi, i am using this into a typescript project getting this error.

import * as React from "react";
import makeAsyncScriptLoader from "react-async-script";

const callbackName = "onloadcallback";
const URL = `https://maps.googleapis.com/maps/api/js?key=mykey&libraries=places&callback=${callbackName}`;
const globalName = "gmap";

class MapWrapper extends React.Component<{asyncScriptOnLoad?:()=>void},{}>{
  render(){
    return <div></div>
  }
}
export default makeAsyncScriptLoader(MapWrapper, URL, {
  callbackName: callbackName,
  globalName: globalName,
});

Cont. clean up bundled polyfills

As a follow up to #26. Option 2 implementation tracking with this issue 😸:

  • just transpile source code to es5+/es6? and not polyfill.
    • Update the readme to make note of which features the user should polyfill if needed

After updating after pr #27 to react-google-recaptcha@^0.13.0 which uses this library at react-async-script ">=0.10.0" the bundled polyfills dropped from 21kb parsed / 7.5kb gzip to 15kb parsed / 5.5kb gzip.

Great first step, but I'd argue that is still way too much, especially with browser support for es5+ at all time highs.

Those sizes above include both react-google-recaptcha and react-async-script bundled polyfills.

Use it for OEmbed, it's great, however, I found a weird issue...

For OEmbed, a js file is used to convert the markup. So I use this package to async load the js. It works when the page get refreshed. But after client navigation, the markup has NOT been converted by the js, even though the js did get loaded, seems the js doesn't execute.
For both cases, no callback is used.
Any idea? (By the way, I use next.js)

How to change URL dynamically ?

I would like to pass a parameter to script. Is there a way to do this?

export default ReactAsyncScript(
     Component,
     `https://scripturl.com/?first_name=${configurable}`,  
     {
        globalName: 'uniqueName',
        removeOnUnmount: true,
     }
)

Have babel-runtime as a peer dependency

Hi,

Would it be possible to have babel-runtime as a peer dependency? I'm currently using 6.9.0 in my project and since this project requires ^5.8.0, I end up with babel-runtime twice in my bundle.

By having it as a peer dep, it would allow developers to provide their own and reuse dependencies.

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.