Isolated JavaScript in Blazor with collocated JS files
If you’ve worked with Blazor and JS Interop before, you’ve probably ended up with a massive app.js file full of random functions for different components. It works, but it gets messy fast. Luckily there’s a much cleaner approach: collocated JavaScript files.
The idea
Just like CSS isolation, Blazor lets you place a .razor.js file next to your component. The JavaScript module gets loaded on demand, only when the component actually needs it. No global scripts, no pollution.
Setting it up
Let’s say we have a Clipboard.razor component that copies text to the clipboard. Create a file called Clipboard.razor.js right next to it:
export function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
console.log("Copied to clipboard!");
});
}
Notice the export keyword — this is important. Blazor loads it as a standard ES module.
Loading the module in Blazor
In your component, you use IJSRuntime to import the module. The path follows a convention: ./_content/{ASSEMBLY_NAME}/{COMPONENT_PATH}.razor.js for libraries, or just the relative path for the current project.
@inject IJSRuntime JS
@implements IAsyncDisposable
<button @onclick="Copy">Copy to clipboard</button>
@code {
[Parameter]
public string Text { get; set; }
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./Components/Clipboard.razor.js");
}
}
private async Task Copy()
{
if (module is not null)
{
await module.InvokeVoidAsync("copyToClipboard", Text);
}
}
public async ValueTask DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
}
A few things to note here:
- We load the module in
OnAfterRenderAsyncbecause JS Interop isn’t available during server-side prerendering - We keep a reference to the module with
IJSObjectReference - We implement
IAsyncDisposableto clean up the module when the component is destroyed
Why this is better
Before, I used to dump everything into a single wwwroot/js/app.js. It worked, but finding functions was a pain, and every page loaded JavaScript it didn’t need. With collocated JS files:
- Each component owns its own JavaScript
- Modules are loaded lazily, only when needed
- No global function name conflicts
- Easier to maintain and delete — when you delete the component, you delete the JS with it
A gotcha with the path
The path you pass to import depends on whether you’re in a standalone Blazor app or a Razor Class Library. For a regular Blazor app, the path is relative to wwwroot. The .razor.js file gets copied there at build time, so you reference it from the component’s location in the project.
If you’re getting a 404 when loading the module, double check the path and make sure the file ends in .razor.js, not just .js.
Hope you liked the post! Feel free to contact me on any social media at @emimontesdeoca.