My custom blazor component uses an external script (hosted on a CDN). Usually one would reference it in index.html
, but I don't want to do that - I want the component to be self contained and easy to use.
The typical solution is a script loader. A popular implementation has been floating around for years: here and here.
wwwroot/js/scriptLoader.js
(referenced in index.html
):
let _loadedScripts = []; // list of loading / loaded scripts
export async function loadScript(src) {
// only load script once
if (_loadedScripts[src])
return Promise.resolve();
return new Promise(function(resolve, reject) {
let tag = document.createElement('script');
tag.type = 'text/javascript';
tag.src = src;
// mark script as loading/loaded
_loadedScripts[src] = true;
tag.onload = function() {
resolve();
}
tag.onerror = function() {
console.error('Failed to load script.');
reject(src);
}
document.body.appendChild(tag);
});
}
Then I create a component where I want to load a custom script.
FooComponent.razor
:
@inject IJSRuntime _js
// etc...
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
// load component's script (using script loader)
await _js.InvokeVoidAsync("loadScript", "https://cdn.example.com/foo.min.js");
}
// invoke `doFoo()` from 'foo.min.js' script
await _js.InvokeVoidAsync("doFoo", "hello world!");
}
That works. I can use the <FooComponent />
and it will load its own script file.
But if I use the component multiple times, I run into a race condition:
- instance 1 of the component
- tries to load the script
- script loader loads the script
- the component can use it
- instances 2+ of the component
- they are loading at the same time as instance 1!
- each tries to load the script, but the loader refuses to load it more than once
- they immediately try to use the script - but it's still busy loading!
- so they all fail and the app crashes (exceptions, etc.)
How can I refactor the code to ensure that the script is actually finished loading, before using it?