Blazor JavaScript Interop
|
This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14. This section’s bibliography lists the reference material consulted while preparing these pages. |
Blazor calls JavaScript for anything the .NET runtime cannot do itself — wrapping an existing JS library, DOM
APIs with no C# equivalent, browser-only features — through IJSRuntime, and JavaScript calls back into .NET
through [JSInvokable].
IJSRuntime
@inject IJSRuntime JS
@code {
private async Task CopyAsync(string text)
=> await JS.InvokeVoidAsync("navigator.clipboard.writeText", text);
private async Task<int> GetWidthAsync()
=> await JS.InvokeAsync<int>("eval", "document.body.clientWidth");
}
InvokeAsync<T> returns a value, InvokeVoidAsync does not. Both are asynchronous even under Interactive
Server, where the call is a round trip over the SignalR circuit. See
JavaScript interoperability
(JS interop).
IJSObjectReference and JS modules
Load an ES module and keep a reference to it instead of calling eval/global functions — the idiomatic way to
wrap a library or a page’s own script:
// wwwroot/js/chart.js
export function render(element, data) { /* draw the chart */ }
export function destroy(element) { /* cleanup */ }
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/chart.js");
await module.InvokeVoidAsync("render", chartElement, data);
}
}
public async ValueTask DisposeAsync()
{
if (module is not null)
{
await module.InvokeVoidAsync("destroy", chartElement);
await module.DisposeAsync();
}
}
Collocated JS
A file named Component.razor.js next to Component.razor is a JS module scoped to that component, served
automatically and loaded the same way as any other module — no manual wwwroot placement needed:
// Chart.razor.js
export function render(element, data) { /* ... */ }
module = await JS.InvokeAsync<IJSObjectReference>("import", "./Components/Chart.razor.js");
JS to .NET: [JSInvokable] and DotNetObjectReference
public sealed partial class NotificationBell : IAsyncDisposable
{
private DotNetObjectReference<NotificationBell>? selfRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
selfRef = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("notifications.subscribe", selfRef);
}
}
[JSInvokable]
public void OnNotificationReceived(string message)
{
latest = message;
StateHasChanged();
}
public ValueTask DisposeAsync()
{
selfRef?.Dispose();
return ValueTask.CompletedTask;
}
}
[JSInvokable] marks an instance method callable via a DotNetObjectReference, or a static method
callable globally via DotNet.invokeMethodAsync("AssemblyName", "MethodName", …) from JS with no reference
needed. Always dispose a DotNetObjectReference — it pins the .NET object alive for JS to call back into.
InvokeConstructorAsync, GetValue/SetValue (.NET 10)
hand-written wrapper function:
var mapRef = await JS.InvokeConstructorAsync<IJSObjectReference>("L.Map", containerElement);
await mapRef.SetValueAsync("zoom", 12);
var currentZoom = await mapRef.GetValueAsync<int>("zoom");
Previously this required a small JS shim (export function createMap(el) { return new L.Map(el); }) for every
constructor or property access; these APIs cover the common case directly.
Synchronous interop in WebAssembly
Interactive WebAssembly runs in the same browser tab as the JS it calls, so it can skip the async round trip where startup latency matters:
@inject IJSInProcessRuntime JS
private int GetWidth() => JS.Invoke<int>("eval", "document.body.clientWidth");
IJSInProcessRuntime is available only under Interactive WebAssembly — injecting it under Interactive Server
throws, since there is no synchronous path over a network connection.
ElementReference and FocusAsync
Capture a reference to a rendered DOM element with @ref to pass it into JS interop calls, or use the built-in
FocusAsync extension for the common case of moving keyboard focus without any custom JS:
<input @ref="searchBox" />
@code {
private ElementReference searchBox;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) await searchBox.FocusAsync();
}
}
Wrapping an existing JS library
The pattern is consistent regardless of the library: write a small JS module exposing the operations the
component needs, import it once, keep the IJSObjectReference, and dispose it (and the underlying JS object,
if the library needs explicit teardown) in DisposeAsync — exactly as shown for the chart example above.
Interop and prerendering
JS interop calls fail during prerendering (see
the double-render trap) because there is no browser
DOM yet to call into — document, window, and any injected library are simply undefined. Guard interop calls
to run only in OnAfterRender(Async) (which never runs during prerendering) rather than in
OnInitializedAsync, or check RendererInfo.IsInteractive first. See
Prerender ASP.NET Core Razor
components.