import { React } from '@gravityforms/libraries';
const { useState, useEffect } = React;
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
// Previously loading/loaded scripts and their current status
export const scripts = {};
// Check for existing <script> tags with this src. If so, update scripts[src]
// and return the new status; otherwise, return undefined.
const checkExisting = ( src ) => {
const existing = document.querySelector(
`script[src="${ src }"]`,
);
if ( existing ) {
// Assume existing <script> tag is already loaded,
// and cache that data for future use.
return ( scripts[ src ] = {
loading: false,
error: null,
scriptEl: existing,
} );
}
return undefined;
};
/**
* @module useScript
* @description A hook to dynamically load an external script and know when it's loaded.
*
* @since 1.0.3
*
* @param {string} src The src for the external script to load.
* @param {boolean} checkForExisting The hook automatically handles when the script was already loaded (or started loading)
* from another instance of the hook. This is for looking for the script existing in the dom from outside our code.
* @param {object} attributes Additional attributes.
*
* @return {*} Either loading or error states.
*
* @example
* import React from 'react';
* import { StripeProvider } from 'react-stripe-elements';
* import useScript from '@gravityforms/react-utils';
*
* import MyCheckout from './my-checkout';
*
* function App() {
* const [loading, error] = useScript({ src: 'https://js.stripe.com/v3/' });
*
* if (loading) return <h3>Loading Stripe API...</h3>;
* if (error) return <h3>Failed to load Stripe API: {error.message}</h3>;
*
* return (
* <StripeProvider apiKey="pk_test_6pRNASCoBOKtIshFeQd4XMUh">
* <MyCheckout />
* </StripeProvider>
* );
* }
*
* export default App;
*
*/
export default function useScript( {
src,
checkForExisting = false,
...attributes
} ) {
// Check whether some instance of this hook considered this src.
let status = src ? scripts[ src ] : undefined;
// If requested, check for existing <script> tags with this src
// (unless we've already loaded the script ourselves).
if ( ! status && checkForExisting && src && isBrowser ) {
status = checkExisting( src );
}
const [ loading, setLoading ] = useState(
status ? status.loading : Boolean( src ),
);
const [ error, setError ] = useState(
status ? status.error : null,
);
useEffect( () => {
// Nothing to do on server, or if no src specified, or
// if loading has already resolved to "loaded" or "error" state.
if ( ! isBrowser || ! src || ! loading || error ) {
return;
}
// Check again for existing <script> tags with this src
// in case it's changed since mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
status = scripts[ src ];
if ( ! status && checkForExisting ) {
status = checkExisting( src );
}
// Determine or create <script> element to listen to.
let scriptEl;
if ( status ) {
scriptEl = status.scriptEl;
} else {
scriptEl = document.createElement( 'script' );
scriptEl.src = src;
Object.keys( attributes ).forEach( ( key ) => {
if ( scriptEl[ key ] === undefined ) {
scriptEl.setAttribute( key, attributes[ key ] );
} else {
scriptEl[ key ] = attributes[ key ];
}
} );
status = scripts[ src ] = {
loading: true,
error: null,
scriptEl,
};
}
// `status` is now guaranteed to be defined: either the old status
// from a previous load, or a newly created one.
const handleLoad = () => {
if ( status ) {
status.loading = false;
}
setLoading( false );
};
const handleError = ( err ) => {
if ( status ) {
status.error = err;
}
setError( err );
};
scriptEl.addEventListener( 'load', handleLoad );
scriptEl.addEventListener( 'error', handleError );
document.body.appendChild( scriptEl );
return () => {
scriptEl.removeEventListener( 'load', handleLoad );
scriptEl.removeEventListener( 'error', handleError );
};
// we need to ignore the attributes as they're a new object per call, so we'd never skip an effect call
}, [ src ] );
return [ loading, error ];
}