blazor-js-isolation-modules-import

Isolated JavaScript Modules in Blazor

  • 3 min

A JavaScript module is a file that exports an explicit API without adding its functions to the global scope.

Blazor leverages this native browser feature to provide us with JS Isolation.

Advantages:

  1. Cleanliness: You don’t pollute the window.
  2. Performance: The script is not downloaded when the app starts. It is downloaded (Lazy Load) only when the user enters the page that uses that component.
  3. Encapsulation: Internal functions and variables remain inside the module (although the same module instance can be shared by multiple imports).

Creating the JavaScript Module

Let’s create a script for an editor. Instead of adding it to window, we use the export keyword.

Create a file in wwwroot/js/editor.js:

// wwwroot/js/editor.js

export function initEditor(element, initialMessage) {
    if (element) {
        element.value = initialMessage;
        element.style.border = "2px solid #6610f2"; // Simulating something visual
        console.log("Editor initialized");
    }
}

export function getText(element) {
    return element ? element.value : "";
}

export function showAlert() {
    alert("Hello from an isolated module!");
}
Copied!

Notice there is no window.something = .... Everything you want to use from C# must have export before it.

Importing the Module with IJSObjectReference

Here the mechanics change compared to previous articles. Instead of calling JS.InvokeVoidAsync directly on a global function, we must first import the file.

When you dynamically import a JS file, Blazor returns an object of type IJSObjectReference. This object is our “remote control” to interact with that specific file.

@implements IAsyncDisposable
@inject IJSRuntime JS

<textarea @ref="editor" rows="5"></textarea>
<button @onclick="ReadText">Read Text</button>

@code {
    // This variable will hold the reference to the loaded module
    private IJSObjectReference? moduleJS;
    private ElementReference editor;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // 1. Load the file dynamically
            // The path is relative to wwwroot
            moduleJS = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./js/editor.js");

            // 2. Invoke the exported function FROM THE MODULE
            await moduleJS.InvokeVoidAsync("initEditor", editor, "Write here...");
        }
    }

    private async Task ReadText()
    {
        if (moduleJS is not null)
        {
            var text = await moduleJS.InvokeAsync<string>("getText", editor);
            Console.WriteLine($"Retrieved text: {text}");
        }
    }

    // ... Dispose (see below)
}
Copied!

Notice the key difference:

  • Before: JS.InvokeVoidAsync("globalName")
  • Now: moduleJS.InvokeVoidAsync("exportedName")

Releasing the Reference with DisposeAsync

IJSObjectReference represents an interop reference to the module. We must release it when the component is removed so that the runtime can remove it from its reference table.

To do this, the component must implement IAsyncDisposable (not regular IDisposable, because JS cleanup is asynchronous).

public async ValueTask DisposeAsync()
{
    if (moduleJS is not null)
    {
        try
        {
            await moduleJS.DisposeAsync();
        }
        catch (JSDisconnectedException)
        {
            // Ignore: this happens if the user closes the browser tab
            // and SignalR disconnects before we can clean up.
        }
    }
}
Copied!

JavaScript Files Placed Alongside the Component

In .NET 6+, Microsoft improved this even further. Just as we can have Counter.razor.css, we can have Counter.razor.js.

If you place the JS file right next to the component (in the Components or Pages folder, not in wwwroot), Blazor will automatically bundle it into a special path.

File Structure:

  • Pages/MyComponent.razor
  • Pages/MyComponent.razor.js

How to load it: In the application project, the file is imported via its public path within the project itself:

moduleJS = await JS.InvokeAsync<IJSObjectReference>(
    "import", "./Components/Pages/MyComponent.razor.js");
Copied!

If the component belongs to a Razor Class Library, the path uses ./_content/{PACKAGE_ID}/... before the file path.

Watch out for caching: During development, browsers sometimes aggressively cache JS modules. If you change the JS code and don’t see changes, try a “Hard Refresh” (Ctrl + F5) or open the DevTools and check “Disable Cache”.