Cyril F
Cyril F

Reputation: 1882

How can React useScript hook handles multiple calls

I'm using the React useScript hook (from useHooks website). It allows to easily load external scripts and cache them once loaded.

It works fine however I found an edge-case causing me some issues.. The problem is with the caching of scripts.

If I have a component loaded 2 times in a page using useScript as below:

const ScriptDemo = src => {
  const [loaded, error] = useScript("https://hzl7l.codesandbox.io/test-external-script.js");

  return (
    <div>
      <div>
        Script loaded: <b>{loaded.toString()}</b>
      </div>
      <br />
      {loaded && !error && (
        <div>
          Script function call response: <b>{TEST_SCRIPT.start()}</b>
        </div>
      )}
    </div>
  );
};

function App() {
  return (
    <div>
      <ScriptDemo />
      <ScriptDemo />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

You can see and reproduce here: https://codesandbox.io/s/usescript-hzl7l

If my App only have one ScriptDemo it's fine, however having two or more would make it fails.

Indeed the flow will be:

One way to fix it is to change the useScript hook to only cache the script after a successful onScriptLoad callback. The issue with this approach is that the external script will be called twice. See here: https://codesandbox.io/s/usescript-0yior

I thought about caching the script src AND a loading boolean but then it implies setting up a timeout handling and it gets very complex in my opinion.

So, what is the best way to update the hook in order to load the external script only once but ensuring it's correctly loaded?

Upvotes: 2

Views: 3897

Answers (1)

ckedar
ckedar

Reputation: 1909

In useScript module, we will need to keep track of status of loading the script.

So instead of cachedScripts being simple array of strings, we now need to keep an object representing the status of loading.

This modified implementation of useScript will address the issue:

import { useState, useEffect } from 'react';

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

  useEffect(
    () => {
      const onScriptLoad = () => {
        cachedScripts[src].loaded = true;
        setState({
          loaded: true,
          error: false
        });
      };

      const onScriptError = () => {
        // Remove it from cache, so that it can be re-attempted if someone tries to load it again
        delete cachedScripts[src];

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

      let scriptLoader = cachedScripts[src];
      if(scriptLoader) { // Loading was attempted earlier
        if(scriptLoader.loaded) { // Script was successfully loaded
          setState({
            loaded: true,
            error: false
          });
        } else { //Script is still loading
          let script = scriptLoader.script;
          script.addEventListener('load', onScriptLoad);
          script.addEventListener('error', onScriptError);
          return () => {
            script.removeEventListener('load', onScriptLoad);
            script.removeEventListener('error', onScriptError);
          };
        }
      } else {
        // Create script
        let script = document.createElement('script');
        script.src = src;
        script.async = true;

        // Script event listener callbacks for load and error


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

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

        cachedScripts[src] = {loaded:false, 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];
}

Edit:

Went to GitHub page of useHooks to suggest this improvement and found some one has already posted similar fix:

https://gist.github.com/gragland/929e42759c0051ff596bc961fb13cd93#gistcomment-2975113

Upvotes: 3

Related Questions