Reputation: 1882
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
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