Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import { useState, useEffect } from 'react';
// Usage
function App() {
const [loaded, error] = useScript(
'https://pm28k14qlj.codesandbox.io/test-external-script.js'
);
return (
<div>
<div>
Script loaded: <b>{loaded.toString()}</b>
</div>
{loaded && !error && (
<div>
Script function call response: <b>{TEST_SCRIPT.start()}</b>
</div>
)}
</div>
);
}
// Hook
let cachedScripts = [];
function useScript(src) {
// Keeping track of script loaded and error state
const [state, setState] = useState({
loaded: false,
error: false
});
useEffect(
() => {
// If cachedScripts array already includes src that means another instance ...
// ... of this hook already loaded this script, so no need to load again.
if (cachedScripts.includes(src)) {
setState({
loaded: true,
error: false
});
} else {
cachedScripts.push(src);
// Create script
let script = document.createElement('script');
script.src = src;
script.async = true;
// Script event listener callbacks for load and error
const onScriptLoad = () => {
setState({
loaded: true,
error: false
});
};
const onScriptError = () => {
// Remove from cachedScripts we can try loading again
const index = cachedScripts.indexOf(src);
if (index >= 0) cachedScripts.splice(index, 1);
script.remove();
setState({
loaded: true,
error: true
});
};
script.addEventListener('load', onScriptLoad);
script.addEventListener('error', onScriptError);
// Add script to document body
document.body.appendChild(script);
// Remove event listeners on cleanup
return () => {
script.removeEventListener('load', onScriptLoad);
script.removeEventListener('error', onScriptError);
};
}
},
[src] // Only re-run effect if script src changes
);
return [state.loaded, state.error];
}
@gokulkrishh

This comment has been minimized.

Copy link

commented Nov 16, 2018

In line no 58, loaded should be false ? if there is a error while loading.

@gragland

This comment has been minimized.

Copy link
Owner Author

commented Nov 16, 2018

@gokulkrishh That just indicates that the script is no longer loading, but I can see how that's a bit confusing. Will update if I think of a better way to do it, but open to suggestions :)

@tikotzky

This comment has been minimized.

Copy link

commented Nov 18, 2018

It seems that if the script is cached then state doesn’t get updated and loading will stay false.

@gragland

This comment has been minimized.

Copy link
Owner Author

commented Nov 18, 2018

@tikotzky Woops, good catch! Updated the code with a fix.

@mohammedzamakhan

This comment has been minimized.

Copy link

commented Nov 19, 2018

it can just return state, instead of [state.loaded, state.error], considering https://twitter.com/_developit/status/1057636803354648582

@stunaz

This comment has been minimized.

Copy link

commented Jan 19, 2019

concurrency issue

@webOS101

This comment has been minimized.

Copy link

commented May 4, 2019

Seems like the script status should be stored in the cache so that it doesn't get marked as loaded if the same script is attempted during loading.

@AegisToast

This comment has been minimized.

Copy link

commented Jun 6, 2019

Just came across this and had a few thoughts.

it can just return state, instead of [state.loaded, state.error], considering https://twitter.com/_developit/status/1057636803354648582

Even if it's faster, that's not a great practice because it causes needless re-renders on the subscribed component any time the state object is changed. It would work better with useReducer instead of useState, but then you're adding a reducer function to justify the imperceptible speed boost.

concurrency issue

There shouldn't be any issues with concurrency or duplicate scripts being loaded, since src is added to cachedScripts as soon as it's received.

Seems like the script status should be stored in the cache so that it doesn't get marked as loaded if the same script is attempted during loading.

Definitely an issue. If Component A is loading a script, and then Component B comes along and tries to load it, Component B will think it's loaded before it actually is. However, you can't just add the loaded state to the cachedScripts array, you have to also add callbacks. Otherwise, in the above example, Component B would never update its state with the script is finished loading.

Another problem with the script: If src is changed (unlikely, but plausible), the state is never reset, so the component will always think the script is already loaded. The simple solution is to add this after line 41:

setState({loaded: false, error: false})
@JureSotosek

This comment has been minimized.

Copy link

commented Jul 19, 2019

A few problems I've encountered with this hook:

  1. If I try to use useScript with the same script two different times in a short period of time, one useScript is going to say that the script is loaded, even before it actually is loaded since the other will push the src of the script to cachedScripts before the loading ends. (Look at L42)

    This seems like it can be fixed by just moving the cachedScripts.push(src) to after the script has been loaded, but that introduces another problem, where the same script can be included two different times, which can break some scripts.

  2. If I quickly mount and then unmount a useScript.

    The src will be pushed to cachedScripts, but if then an error occurs, it will never be removed from cachedScripts, since on unmount you remove the error eventListener.


I fixed all of those issues by introducing another loadingScripts variable as shown below 👇🏼

Keep in mind that loadingScripts holds actual script objects and not just srcs.

// Hook
let cachedScripts = [];
let loadingScripts = [];
function useScript(src) {
  // Keeping track of script loaded and error state
  const [state, setState] = useState({
    loaded: false,
    error: false
  });

  useEffect(
    () => {
      // If cachedScripts array already includes src that means another instance ...
      // ... of this hook already loaded this script, so no need to load again.
      if (cachedScripts.includes(src)) {
        setState({
          loaded: true,
          error: false
        });
      } else {
        let script 

        // Create script only if one is not being loaded
        if (loadingScripts[src]) {
          script = loadingScripts[src]
        } else {
          script = document.createElement('script')
          script.src = src
          script.async = true

          // Add script to loadingScripts
          loadingScripts[src] = script

          // Add script to document body
          document.body.appendChild(script)
        }

        // Script event listener callbacks for load and error
        const onScriptLoad = () => {
          // Add script to cachedScripts
          cachedScripts[src] = script

          // Remove from loadingScripts
	  delete loadingScripts[src]

          setState({
            loaded: true,
            error: false
          });
        };

        const onScriptError = () => {
          // Remove from loadingScripts we can try loading again
	  delete loadingScripts[src]
	  script.remove()

          setState({
            loaded: true,
            error: true
          });
        };

        script.addEventListener('load', onScriptLoad);
        script.addEventListener('error', onScriptError);

        // Add script to document body
        document.body.appendChild(script);

        // Remove event listeners on cleanup
        // and remove script from loadingScripts if not yet loaded
        return () => {
           // Remove from loadingScripts
	  delete loadingScripts[src]

          script.removeEventListener('load', onScriptLoad);
          script.removeEventListener('error', onScriptError);
        };
      }
    },
    [src] // Only re-run effect if script src changes
  );

  return [state.loaded, state.error];
}

Hope this helps anybody ✌🏼

@bernard-leech

This comment has been minimized.

Copy link

commented Aug 15, 2019

@JureSotosek when do you push the loaded script src to the cachedScripts array?

@JureSotosek

This comment has been minimized.

Copy link

commented Aug 15, 2019

@JureSotosek when do you push the loaded script src to the cachedScripts array?

@bernard-leech Ahh, mistake, now its added to cachedScripts at onScriptLoad. Be careful with my code since it's not 100% tested. I did some rewriting when posting it here to make it look closer to what the original useScript looked like.

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.