Practical Blazor JavaScript Interop Guide
Mastering JavaScript Interop in Blazor: Efficiently calling JS from C# and vice-versa, managing object references, and ensuring type safety.
Introduction
While Blazor allows us to write C# for the frontend, the reality of the web is that the ecosystem is rich with JavaScript libraries. Whether it’s a complex chart, a map, or a specific browser API, robust interop is essential. It’s not just about “making it work”; it’s about managing memory, ensuring type safety, and handling asynchrony correctly.
Why Proper Interop Matters:
- Memory Leaks: Improper disposal of
DotNetObjectReferenceorIJSObjectReferencecan crash long-running apps. - Performance: Excessive marshalling between WASM/Server and JS can slow down the UI.
- Maintainability: encapsulating JS logic prevents “magic string” calls scattered throughout Razor components.
What We’ll Build
We will implement a wrapper for a hypothetical PDF viewer library. You will learn how to:
- Load Modules: Use dynamic importing of JS modules.
- Two-way Comms: Call JS functions and receive callbacks in C#.
- Clean Up: Implement
IAsyncDisposablecorrectly.
Architecture Overview
We use the “Module Pattern” to isolate our JS code, avoiding global scope pollution.
sequenceDiagram
participant C as C# Component
participant R as IJSRuntime
participant J as JS Module
participant D as DOM
C->>R: InvokeAsync("import", "./pdfViewer.js")
R->>J: Load Module
J-->>C: Return IJSObjectReference
C->>J: InvokeVoidAsync("init", elementId, DotNetRef)
J->>D: Render Viewer
D->>J: User Clicks "Next Page"
J->>C: InvokeMethodAsync("OnPageChanged", 2)
Implementation
1. The JavaScript Module
Create an ES6 module that exports functions.
export function initViewer(element, dotNetRef) {
const viewer = new ThirdPartyViewer(element);
viewer.on('pagechange', (page) => {
dotNetRef.invokeMethodAsync('OnPageChanged', page);
});
return {
next: () => viewer.nextPage(),
prev: () => viewer.prevPage(),
dispose: () => viewer.destroy()
};
} 2. The C# Wrapper Component
We wrap the functionality in a reusable C# class or component.
using Microsoft.JSInterop;
public partial class PdfViewer : IAsyncDisposable
{
[Inject] IJSRuntime JSRuntime { get; set; }
private IJSObjectReference? _module;
private IJSObjectReference? _instance;
private DotNetObjectReference<PdfViewer>? _selfRef;
private ElementReference _container;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_module = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/pdfViewer.js");
_selfRef = DotNetObjectReference.Create(this);
_instance = await _module.InvokeAsync<IJSObjectReference>(
"initViewer", _container, _selfRef);
}
}
[JSInvokable]
public void OnPageChanged(int page)
{
Console.WriteLine($"User is now on page {page}");
}
public async ValueTask DisposeAsync()
{
if (_instance != null)
{
await _instance.InvokeVoidAsync("dispose");
await _instance.DisposeAsync();
}
if (_module != null)
await _module.DisposeAsync();
_selfRef?.Dispose();
}
} Conclusion
By treating JavaScript interoperability as a first-class citizen with strict types and lifecycle management, we extend Blazor’s capabilities infinitely. This pattern defines how BlueRobin integrates complex visualization tools without compromising stability.