Call JavaScript functions from .NET methods in ASP.NET Core Blazor

This article covers invoking JavaScript (JS) functions from .NET. For information on how to call .NET methods from JS, see Call .NET methods from JavaScript functions in ASP.NET Core Blazor.

To call into JS from .NET, inject the IJSRuntime abstraction and call one of the following methods:

For the preceding .NET methods that invoke JS functions:

  • The function identifier (String) is relative to the global scope (window). To call window.someScope.someFunction, the identifier is someScope.someFunction. There's no need to register the function before it's called.
  • Pass any number of JSON-serializable arguments in Object[] to a JS function.
  • The cancellation token (CancellationToken) propagates a notification that operations should be canceled.
  • TimeSpan represents a time limit for a JS operation.
  • The TValue return type must also be JSON serializable. TValue should match the .NET type that best maps to the JSON type returned.
  • A JS Promise is returned for InvokeAsync methods. InvokeAsync unwraps the Promise and returns the value awaited by the Promise.

For Blazor Server apps with prerendering enabled, calling into JS isn't possible during initial prerendering. JS interop calls must be deferred until after the connection with the browser is established. For more information, see the Detect when a Blazor Server app is prerendering section.

The following example is based on TextDecoder, a JS-based decoder. The example demonstrates how to invoke a JS function from a C# method that offloads a requirement from developer code to an existing JS API. The JS function accepts a byte array from a C# method, decodes the array, and returns the text to the component for display.

Add the following JS code inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server):

<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    console.log(decodedArray);
    return decodedArray;
  };
</script>

The following CallJsExample1 component:

  • Invokes the convertArray JS function with InvokeAsync when selecting a button (Convert Array).
  • After the JS function is called, the passed array is converted into a string. The string is returned to the component for display (text).

Pages/CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}

Invoke JavaScript functions without reading a returned value (InvokeVoidAsync)

Use InvokeVoidAsync when:

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server), provide a displayTickerAlert1 JS function. The function is called with InvokeVoidAsync and doesn't return a value:

<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>

Component (.razor) example (InvokeVoidAsync)

TickerChanged calls the handleTickerChanged1 method in the following CallJsExample2 component.

Pages/CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string? stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

Class (.cs) example (InvokeVoidAsync)

JsInteropClasses1.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged calls the handleTickerChanged1 method in the following CallJsExample3 component.

Pages/CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses1? jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
            price = r.Next(1, 101);
            await jsClass.TickerChanged(stockSymbol, price);
        }
    }

    public void Dispose() => jsClass?.Dispose();
}

Invoke JavaScript functions and read a returned value (InvokeAsync)

Use InvokeAsync when .NET should read the result of a JS call.

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server), provide a displayTickerAlert2 JS function. The following example returns a string for display by the caller:

<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>

Component (.razor) example (InvokeAsync)

TickerChanged calls the handleTickerChanged2 method and displays the returned string in the following CallJsExample4 component.

Pages/CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string? stockSymbol;
    private decimal price;
    private string? result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

Class (.cs) example (InvokeAsync)

JsInteropClasses2.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged calls the handleTickerChanged2 method and displays the returned string in the following CallJsExample5 component.

Pages/CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses2? jsClass;
    private string? result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
            price = r.Next(1, 101);
            var interopResult = await jsClass.TickerChanged(stockSymbol, price);
            result = $"Result of TickerChanged call for {stockSymbol} at " +
                $"{price.ToString("c")}: {interopResult}";
        }
    }

    public void Dispose() => jsClass?.Dispose();
}

Dynamic content generation scenarios

For dynamic content generation with BuildRenderTree, use the [Inject] attribute:

[Inject]
IJSRuntime JS { get; set; }

Detect when a Blazor Server app is prerendering

This section applies to Blazor Server and hosted Blazor WebAssembly apps that prerender Razor components. Prerendering is covered in Prerender and integrate ASP.NET Core Razor components.

While an app is prerendering, certain actions, such as calling into JavaScript, aren't possible. Components may need to render differently when prerendered.

For the following example, the setElementText1 function is placed inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server). The function is called with JSRuntimeExtensions.InvokeVoidAsync and doesn't return a value:

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

Warning

The preceding example modifies the Document Object Model (DOM) directly for demonstration purposes only. Directly modifying the DOM with JavaScript isn't recommended in most scenarios because JavaScript can interfere with Blazor's change tracking. For more information, see Blazor JavaScript interoperability (JS interop).

To delay JavaScript interop calls until a point where such calls are guaranteed to work, override the OnAfterRender{Async} lifecycle event. This event is only called after the app is fully rendered.

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

Note

The preceding example pollutes the client with global methods. For a better approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

export setElementText1 = (element, text) => element.innerText = text;

The following component demonstrates how to use JavaScript interop as part of a component's initialization logic in a way that's compatible with prerendering. The component shows that it's possible to trigger a rendering update from inside OnAfterRenderAsync. The developer must be careful to avoid creating an infinite loop in this scenario.

For the following example, the setElementText2 function is placed inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server). The function is called withIJSRuntime.InvokeAsync and returns a value:

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

Warning

The preceding example modifies the Document Object Model (DOM) directly for demonstration purposes only. Directly modifying the DOM with JavaScript isn't recommended in most scenarios because JavaScript can interfere with Blazor's change tracking. For more information, see Blazor JavaScript interoperability (JS interop).

Where JSRuntime.InvokeAsync is called, the ElementReference is only used in OnAfterRenderAsync and not in any earlier lifecycle method because there's no JavaScript element until after the component is rendered.

StateHasChanged is called to rerender the component with the new state obtained from the JavaScript interop call (for more information, see ASP.NET Core Blazor component rendering). The code doesn't create an infinite loop because StateHasChanged is only called when data is null.

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string? data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

Note

The preceding example pollutes the client with global methods. For a better approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

Location of JavaScipt

Load JavaScript (JS) code using any of approaches described by the JavaScript (JS) interoperability (interop) overview article:

For information on isolating scripts in JS modules, see the JavaScript isolation in JavaScript modules section.

Warning

Don't place a <script> tag in a component file (.razor) because the <script> tag can't be updated dynamically.

JavaScript isolation in JavaScript modules

Blazor enables JavaScript (JS) isolation in standard JavaScript modules (ECMAScript specification).

JS isolation provides the following benefits:

  • Imported JS no longer pollutes the global namespace.
  • Consumers of a library and components aren't required to import the related JS.

For example, the following JS module exports a JS function for showing a browser window prompt. Place the following JS code in an external JS file.

wwwroot/scripts.js:

export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

Add the preceding JS module to an app or class library as a static web asset in the wwwroot folder and then import the module into the .NET code by calling InvokeAsync on the IJSRuntime instance.

IJSRuntime imports the module as an IJSObjectReference, which represents a reference to a JS object from .NET code. Use the IJSObjectReference to invoke exported JS functions from the module.

Pages/CallJsExample6.razor:

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

In the preceding example:

  • By convention, the import identifier is a special identifier used specifically for importing a JS module.
  • Specify the module's external JS file using its stable static web asset path: ./{SCRIPT PATH AND FILENAME (.js)}, where:
    • The path segment for the current directory (./) is required in order to create the correct static asset path to the JS file.
    • The {SCRIPT PATH AND FILENAME (.js)} placeholder is the path and file name under wwwroot.
  • Disposes the IJSObjectReference for garbage collection in IAsyncDisposable.DisposeAsync.

Dynamically importing a module requires a network request, so it can only be achieved asynchronously by calling InvokeAsync.

IJSInProcessObjectReference represents a reference to a JS object whose functions can be invoked synchronously.

Note

When the external JS file is supplied by a Razor class library, specify the module's JS file using its stable static web asset path: ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)}:

  • The path segment for the current directory (./) is required in order to create the correct static asset path to the JS file.
  • The {PACKAGE ID} placeholder is the library's package ID. The package ID defaults to the project's assembly name if <PackageId> isn't specified in the project file. In the following example, the library's assembly name is ComponentLibrary and the library's project file doesn't specify <PackageId>.
  • The {SCRIPT PATH AND FILENAME (.js)} placeholder is the path and file name under wwwroot. In the following example, the external JS file (script.js) is placed in the class library's wwwroot folder.
var module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

For more information, see Consume ASP.NET Core Razor components from Razor class libraries.

Capture references to elements

Some JavaScript (JS) interop scenarios require references to HTML elements. For example, a UI library may require an element reference for initialization, or you might need to call command-like APIs on an element, such as click or play.

Capture references to HTML elements in a component using the following approach:

  • Add an @ref attribute to the HTML element.
  • Define a field of type ElementReference whose name matches the value of the @ref attribute.

The following example shows capturing a reference to the username <input> element:

<input @ref="username" ... />

@code {
    private ElementReference username;
}

Warning

Only use an element reference to mutate the contents of an empty element that doesn't interact with Blazor. This scenario is useful when a third-party API supplies content to the element. Because Blazor doesn't interact with the element, there's no possibility of a conflict between Blazor's representation of the element and the Document Object Model (DOM).

In the following example, it's dangerous to mutate the contents of the unordered list (ul) because Blazor interacts with the DOM to populate this element's list items (<li>) from the Todos object:

<ul @ref="MyList">
    @foreach (var item in Todos)
    {
        <li>@item.Text</li>
    }
</ul>

If JS interop mutates the contents of element MyList and Blazor attempts to apply diffs to the element, the diffs won't match the DOM.

For more information, see Blazor JavaScript interoperability (JS interop).

An ElementReference is passed through to JS code via JS interop. The JS code receives an HTMLElement instance, which it can use with normal DOM APIs. For example, the following code defines a .NET extension method (TriggerClickEvent) that enables sending a mouse click to an element.

The JS function clickElement creates a click event on the passed HTML element (element):

window.interopFunctions = {
  clickElement : function (element) {
    element.click();
  }
}

To call a JS function that doesn't return a value, use JSRuntimeExtensions.InvokeVoidAsync. The following code triggers a client-side click event by calling the preceding JS function with the captured ElementReference:

@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await JS.InvokeVoidAsync(
            "interopFunctions.clickElement", exampleButton);
    }
}

To use an extension method, create a static extension method that receives the IJSRuntime instance:

public static async Task TriggerClickEvent(this ElementReference elementRef, 
    IJSRuntime js)
{
    await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}

The clickElement method is called directly on the object. The following example assumes that the TriggerClickEvent method is available from the JsInteropClasses namespace:

@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await exampleButton.TriggerClickEvent(JS);
    }
}

Important

The exampleButton variable is only populated after the component is rendered. If an unpopulated ElementReference is passed to JS code, the JS code receives a value of null. To manipulate element references after the component has finished rendering, use the OnAfterRenderAsync or OnAfterRender component lifecycle methods.

When working with generic types and returning a value, use ValueTask<TResult>:

public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef, 
    IJSRuntime js)
{
    return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}

The {JAVASCRIPT FUNCTION} placeholder is the JS function identifier.

GenericMethod is called directly on the object with a type. The following example assumes that the GenericMethod is available from the JsInteropClasses namespace:

@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}

Reference elements across components

An ElementReference can't be passed between components because:

For a parent component to make an element reference available to other components, the parent component can:

  • Allow child components to register callbacks.
  • Invoke the registered callbacks during the OnAfterRender event with the passed element reference. Indirectly, this approach allows child components to interact with the parent's element reference.

Add the following style to the <head> of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server):

<style>
    .red { color: red }
</style>

Add the following script inside closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server):

<script>
  function setElementClass(element, className) {
    var myElement = element;
    myElement.classList.add(className);
  }
</script>

Pages/CallJsExample7.razor (parent component):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="@this" Title="How is Blazor working for you?" />

Pages/CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}

In the preceding example, the namespace of the app is BlazorSample with components in the Pages folder. If testing the code locally, update the namespace.

Shared/SurveyPrompt.razor (child component):

@inject IJSRuntime JS

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string? Title { get; set; }
}

Shared/SurveyPrompt.razor.cs:

using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable? subscription = null;

        [Parameter]
        public IObservable<ElementReference>? Parent { get; set; }

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = 
                Parent is not null ? Parent.Subscribe(this) : null;
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}

In the preceding example, the namespace of the app is BlazorSample with shared components in the Shared folder. If testing the code locally, update the namespace.

Harden JavaScript interop calls

This section primarily applies to Blazor Server apps, but Blazor WebAssembly apps may also set JS interop timeouts if conditions warrant it.

In Blazor Server apps, JavaScript (JS) interop may fail due to networking errors and should be treated as unreliable. By default, Blazor Server apps use a one minute timeout for JS interop calls. If an app can tolerate a more aggressive timeout, set the timeout using one of the following approaches.

Set a global timeout in the Program.cs with CircuitOptions.JSInteropDefaultCallTimeout:

builder.Services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

The {TIMEOUT} placeholder is a TimeSpan (for example, TimeSpan.FromSeconds(80)).

Set a per-invocation timeout in component code. The specified timeout overrides the global timeout set by JSInteropDefaultCallTimeout:

var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1" });

In the preceding example:

  • The {TIMEOUT} placeholder is a TimeSpan (for example, TimeSpan.FromSeconds(80)).
  • The {ID} placeholder is the identifier for the function to invoke. For example, the value someScope.someFunction invokes the function window.someScope.someFunction.

Although a common cause of JS interop failures are network failures in Blazor Server apps, per-invocation timeouts can be set for JS interop calls in Blazor WebAssembly apps. Although no SignalR circuit exists in a Blazor WebAssembly app, JS interop calls might fail for other reasons that apply in Blazor WebAssembly apps.

For more information on resource exhaustion, see Threat mitigation guidance for ASP.NET Core Blazor Server.

Avoid circular object references

Objects that contain circular references can't be serialized on the client for either:

  • .NET method calls.
  • JavaScript method calls from C# when the return type has circular references.

JavaScript libraries that render UI

Sometimes you may wish to use JavaScript (JS) libraries that produce visible user interface elements within the browser Document Object Model (DOM). At first glance, this might seem difficult because Blazor's diffing system relies on having control over the tree of DOM elements and runs into errors if some external code mutates the DOM tree and invalidates its mechanism for applying diffs. This isn't a Blazor-specific limitation. The same challenge occurs with any diff-based UI framework.

Fortunately, it's straightforward to embed externally-generated UI within a Razor component UI reliably. The recommended technique is to have the component's code (.razor file) produce an empty element. As far as Blazor's diffing system is concerned, the element is always empty, so the renderer does not recurse into the element and instead leaves its contents alone. This makes it safe to populate the element with arbitrary externally-managed content.

The following example demonstrates the concept. Within the if statement when firstRender is true, interact with unmanagedElement outside of Blazor using JS interop. For example, call an external JS library to populate the element. Blazor leaves the element's contents alone until this component is removed. When the component is removed, the component's entire DOM subtree is also removed.

<h1>Hello! This is a Blazor component rendered at @DateTime.Now</h1>

<div @ref="unmanagedElement"></div>

@code {
    private HtmlElement unmanagedElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            ...
        }
    }
}

Consider the following example that renders an interactive map using open-source Mapbox APIs.

The following JS module is placed into the app or made available from a Razor class library.

Note

To create the Mapbox map, obtain an access token from Mapbox Sign in and provide it where the {ACCESS TOKEN} appears in the following code.

wwwroot/mapComponent.js:

import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';

mapboxgl.accessToken = '{ACCESS TOKEN}';

export function addMapToElement(element) {
  return new mapboxgl.Map({
    container: element,
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [-74.5, 40],
    zoom: 9
  });
}

export function setMapCenter(map, latitude, longitude) {
  map.setCenter([longitude, latitude]);
}

To produce correct styling, add the following stylesheet tag to the host HTML page.

In wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server), add the following <link> element to the <head> element markup:

<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
    rel="stylesheet" />

Pages/CallJsExample8.razor:

@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference? mapModule;
    private IJSObjectReference? mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
    {
        if (mapModule is not null && mapInstance is not null)
        {
            await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, 
                latitude, longitude).AsTask();
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}

The preceding example produces an interactive map UI. The user:

  • Can drag to scroll or zoom.
  • Select buttons to jump to predefined locations.

Mapbox street map of Tokyo, Japan with buttons to select Bristol, United Kingdom and Tokyo, Japan

In the preceding example:

  • The <div> with @ref="mapElement" is left empty as far as Blazor is concerned. The mapbox-gl.js script can safely populate the element and modify its contents. Use this technique with any JS library that renders UI. You can embed components from a third-party JS SPA framework inside Blazor components, as long as they don't try to reach out and modify other parts of the page. It is not safe for external JS code to modify elements that Blazor does not regard as empty.
  • When using this approach, bear in mind the rules about how Blazor retains or destroys DOM elements. The component safely handles button click events and updates the existing map instance because DOM elements are retained where possible by default. If you were rendering a list of map elements from inside a @foreach loop, you want to use @key to ensure the preservation of component instances. Otherwise, changes in the list data could cause component instances to retain the state of previous instances in an undesirable manner. For more information, see using @key to preserve elements and components.
  • The example encapsulates JS logic and dependencies within an ES6 module and loads the module dynamically using the import identifier. For more information, see JavaScript isolation in JavaScript modules.

Byte array support

Blazor supports optimized byte array JS interop that avoids encoding/decoding byte arrays into Base64. The following example uses JS interop to pass a byte array to JavaScript.

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server), provide a receiveByteArray JS function. The function is called with InvokeVoidAsync and doesn't return a value:

<script>
  window.receiveByteArray = (bytes) => {
    let utf8decoder = new TextDecoder();
    let str = utf8decoder.decode(bytes);
    return str;
  };
</script>

Pages/CallJsExample9.razor:

@page "/call-js-example-9"
@inject IJSRuntime JS

<h1>Call JS Example 9</h1>

<p>
    <button @onclick="SendByteArray">Send Bytes</button>
</p>

<p>
    @result
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    private string result;

    private async Task SendByteArray()
    {
        var bytes = new byte[] { 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69,
            0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, 0x2c,
            0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x61, 0x6e, 0x2e, 0x20, 0x4e,
            0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e };

        result = await JS.InvokeAsync<string>("receiveByteArray", bytes);
    }
}

For information on using a byte array when calling .NET from JavaScript, see Call .NET methods from JavaScript functions in ASP.NET Core Blazor.

Size limits on JavaScript interop calls

This section only applies to Blazor Server apps. In Blazor WebAssembly, the framework doesn't impose a limit on the size of JavaScript (JS) interop inputs and outputs.

In Blazor Server, JS interop calls are limited in size by the maximum incoming SignalR message size permitted for hub methods, which is enforced by HubOptions.MaximumReceiveMessageSize (default: 32 KB). JS to .NET SignalR messages larger than MaximumReceiveMessageSize throw an error. The framework doesn't impose a limit on the size of a SignalR message from the hub to a client.

When SignalR logging isn't set to Debug or Trace, a message size error only appears in the browser's developer tools console:

Error: Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'.

When SignalR server-side logging is set to Debug or Trace, server-side logging surfaces an InvalidDataException for a message size error.

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

Error:

System.IO.InvalidDataException: The maximum message size of 32768B was exceeded. The message size can be configured in AddHubOptions.

Increase the limit by setting MaximumReceiveMessageSize in Program.cs. The following example sets the maximum receive message size to 64 KB (64 * 1024) in ASP.NET Core 6.0 or later:

builder.Services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

In Startup.ConfigureServices for versions of ASP.NET Core earlier than 6.0:

services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

Increasing the SignalR incoming message size limit comes at the cost of requiring more server resources, and it exposes the server to increased risks from a malicious user. Additionally, reading a large amount of content in to memory as strings or byte arrays can also result in allocations that work poorly with the garbage collector, resulting in additional performance penalties.

One option for reading large payloads is to send the content in smaller chunks and process the payload as a Stream. This can be used when reading large JSON payloads or if data is available in JS as raw bytes. For an example that demonstrates sending large binary payloads in Blazor Server that uses techniques similar to the InputFile component, see the Binary Submit sample app.

Note

Documentation links to the ASP.NET Core reference source load the repository's main branch, which represents the product unit's current development for the next release of ASP.NET Core. To select the branch for a different release, use the Switch branches or tags dropdown list to select the branch. For example, select the release/5.0 branch for the ASP.NET Core 5.0 release.

Consider the following guidance when developing code that transfers a large amount of data between JS and Blazor in Blazor Server apps:

  • Slice the data into smaller pieces, and send the data segments sequentially until all of the data is received by the server.
  • Don't allocate large objects in JS and C# code.
  • Don't block the main UI thread for long periods when sending or receiving data.
  • Free consumed memory when the process is completed or cancelled.
  • Enforce the following additional requirements for security purposes:
    • Declare the maximum file or data size that can be passed.
    • Declare the minimum upload rate from the client to the server.
  • After the data is received by the server, the data can be:
    • Temporarily stored in a memory buffer until all of the segments are collected.
    • Consumed immediately. For example, the data can be stored immediately in a database or written to disk as each segment is received.

Unmarshalled JavaScript interop

Blazor WebAssembly components may experience poor performance when .NET objects are serialized for JavaScript (JS) interop and either of the following are true:

  • A high volume of .NET objects are rapidly serialized. For example, poor performance may result when JS interop calls are made on the basis of moving an input device, such as spinning a mouse wheel.
  • Large .NET objects or many .NET objects must be serialized for JS interop. For example, poor performance may result when JS interop calls require serializing dozens of files.

IJSUnmarshalledObjectReference represents a reference to an JS object whose functions can be invoked without the overhead of serializing .NET data.

In the following example:

  • A struct containing a string and an integer is passed unserialized to JS.
  • JS functions process the data and return either a boolean or string to the caller.
  • A JS string isn't directly convertible into a .NET string object. The unmarshalledFunctionReturnString function calls BINDING.js_string_to_mono_string to manage the conversion of a JS string.

Note

The following examples aren't typical use cases for this scenario because the struct passed to JS doesn't result in poor component performance. The example uses a small object merely to demonstrate the concepts for passing unserialized .NET data.

Place the following <script> block in wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server). Alternatively, add the JS to an external file referenced inside the closing </body> tag. For example, <script src="{SCRIPT PATH AND FILE NAME (.js)}"></script>, where the {SCRIPT PATH AND FILE NAME (.js)} placeholder is the path and file name of the script.

<script>
  window.returnObjectReference = () => {
    return {
      unmarshalledFunctionReturnBoolean: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return name === "Brigadier Alistair Gordon Lethbridge-Stewart" &&
            year === 1968;
      },
      unmarshalledFunctionReturnString: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return BINDING.js_string_to_mono_string(`Hello, ${name} (${year})!`);
      }
    };
  }
</script>

Warning

The js_string_to_mono_string function name, behavior, and existence is subject to change in a future release of .NET. For example:

  • The function is likely to be renamed.
  • The function itself might be removed in favor of automatic conversion of strings by the framework.

Pages/CallJsExample10.razor:

@page "/call-js-example-10"
@using System.Runtime.InteropServices
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Call JS Example 10</h1>

@if (callResultForBoolean)
{
    <p>JS interop was successful!</p>
}

@if (!string.IsNullOrEmpty(callResultForString))
{
    <p>@callResultForString</p>
}

<p>
    <button @onclick="CallJSUnmarshalledForBoolean">
        Call Unmarshalled JS & Return Boolean
    </button>
    <button @onclick="CallJSUnmarshalledForString">
        Call Unmarshalled JS & Return String
    </button>
</p>

<p>
    <a href="https://www.doctorwho.tv">Doctor Who</a>
    is a registered trademark of the <a href="https://www.bbc.com/">BBC</a>.
</p>

@code {
    private bool callResultForBoolean;
    private string? callResultForString;

    private void CallJSUnmarshalledForBoolean()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForBoolean = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>(
                "unmarshalledFunctionReturnBoolean", GetStruct());
    }

    private void CallJSUnmarshalledForString()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForString = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>(
                "unmarshalledFunctionReturnString", GetStruct());
    }

    private InteropStruct GetStruct()
    {
        return new InteropStruct
        {
            Name = "Brigadier Alistair Gordon Lethbridge-Stewart",
            Year = 1968,
        };
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InteropStruct
    {
        [FieldOffset(0)]
        public string Name;

        [FieldOffset(8)]
        public int Year;
    }
}

If an IJSUnmarshalledObjectReference instance isn't disposed in C# code, it can be disposed in JS. The following dispose function disposes the object reference when called from JS:

window.exampleJSObjectReferenceNotDisposedInCSharp = () => {
  return {
    dispose: function () {
      DotNet.disposeJSObjectReference(this);
    },

    ...
  };
}

Array types can be converted from JS objects into .NET objects using js_typed_array_to_array, but the JS array must be a typed array. Arrays from JS can be read in C# code as a .NET object array (object[]).

Other data types, such as string arrays, can be converted but require creating a new Mono array object (mono_obj_array_new) and setting its value (mono_obj_array_set).

Warning

JS functions provided by the Blazor framework, such as js_typed_array_to_array, mono_obj_array_new, and mono_obj_array_set, are subject to name changes, behavioral changes, or removal in future releases of .NET.

Stream from JavaScript to .NET

Blazor supports streaming data directly from JavaScript to .NET. Streams are requested using the Microsoft.JSInterop.IJSStreamReference interface.

Microsoft.JSInterop.IJSStreamReference.OpenReadStreamAsync returns a Stream and uses the following parameters:

  • maxAllowedSize: Maximum number of bytes permitted for the read operation from JavaScript, which defaults to 512,000 bytes if not specified.
  • cancellationToken: A CancellationToken for cancelling the read.

In JavaScript:

function streamToDotNet() {
  return new Uint8Array(10000000);
}

In C# code:

var dataReference = 
    await JS.InvokeAsync<IJSStreamReference>("streamToDotNet");
using var dataReferenceStream = 
    await dataReference.OpenReadStreamAsync(maxAllowedSize: 10_000_000);

var outputPath = Path.Combine(Path.GetTempPath(), "file.txt");
using var outputFileStream = File.OpenWrite(outputPath);
await dataReferenceStream.CopyToAsync(outputFileStream);

In the preceding example:

  • JS is an injected IJSRuntime instance.
  • The dataReferenceStream is written to disk (file.txt) at the current user's temporary folder path (GetTempPath).

Stream from .NET to JavaScript

Blazor supports streaming data directly from .NET to JavaScript. Streams are created using a Microsoft.JSInterop.DotNetStreamReference.

Microsoft.JSInterop.DotNetStreamReference represents a .NET stream and uses the following parameters:

  • stream: The stream sent to JavaScript.
  • leaveOpen: Determines if the stream is left open after transmission. If a value isn't provided, leaveOpen defaults to false.

In JavaScript, use an array buffer or a readable stream to receive the data:

  • Using an ArrayBuffer:

    async function streamToJavaScript(streamRef) {
      const data = await streamRef.arrayBuffer();
    }
    
  • Using a ReadableStream:

    async function streamToJavaScript(streamRef) {
      const stream = await streamRef.stream();
    }
    

In C# code:

using var streamRef = new DotNetStreamReference(stream: {STREAM}, leaveOpen: false);
await JS.InvokeVoidAsync("streamToJavaScript", streamRef);

In the preceding example:

  • The {STREAM} placeholder represents the Stream sent to JavaScript.
  • JS is an injected IJSRuntime instance.

Catch JavaScript exceptions

To catch JS exceptions, wrap the JS interop in a try-catch block and catch a JSException.

In the following example, the nonFunction JS function doesn't exist. When the function isn't found, the JSException is trapped with a Message that indicates the following error:

Could not find 'nonFunction' ('nonFunction' was undefined).

Pages/CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string? errorMessage;
    private string? result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

Additional resources

This article covers invoking JavaScript (JS) functions from .NET. For information on how to call .NET methods from JS, see Call .NET methods from JavaScript functions in ASP.NET Core Blazor.

To call into JS from .NET, inject the IJSRuntime abstraction and call one of the following methods:

For the preceding .NET methods that invoke JS functions:

  • The function identifier (String) is relative to the global scope (window). To call window.someScope.someFunction, the identifier is someScope.someFunction. There's no need to register the function before it's called.
  • Pass any number of JSON-serializable arguments in Object[] to a JS function.
  • The cancellation token (CancellationToken) propagates a notification that operations should be canceled.
  • TimeSpan represents a time limit for a JS operation.
  • The TValue return type must also be JSON serializable. TValue should match the .NET type that best maps to the JSON type returned.
  • A JS Promise is returned for InvokeAsync methods. InvokeAsync unwraps the Promise and returns the value awaited by the Promise.

For Blazor Server apps with prerendering enabled, calling into JS isn't possible during initial prerendering. JS interop calls must be deferred until after the connection with the browser is established. For more information, see the Detect when a Blazor Server app is prerendering section.

The following example is based on TextDecoder, a JS-based decoder. The example demonstrates how to invoke a JS function from a C# method that offloads a requirement from developer code to an existing JS API. The JS function accepts a byte array from a C# method, decodes the array, and returns the text to the component for display.

Add the following JS code inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    console.log(decodedArray);
    return decodedArray;
  };
</script>

The following CallJsExample1 component:

  • Invokes the convertArray JS function with InvokeAsync when selecting a button (Convert Array).
  • After the JS function is called, the passed array is converted into a string. The string is returned to the component for display (text).

Pages/CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}

Invoke JavaScript functions without reading a returned value (InvokeVoidAsync)

Use InvokeVoidAsync when:

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server), provide a displayTickerAlert1 JS function. The function is called with InvokeVoidAsync and doesn't return a value:

<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>

Component (.razor) example (InvokeVoidAsync)

TickerChanged calls the handleTickerChanged1 method in the following CallJsExample2 component.

Pages/CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

Class (.cs) example (InvokeVoidAsync)

JsInteropClasses1.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged calls the handleTickerChanged1 method in the following CallJsExample3 component.

Pages/CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}

Invoke JavaScript functions and read a returned value (InvokeAsync)

Use InvokeAsync when .NET should read the result of a JS call.

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server), provide a displayTickerAlert2 JS function. The following example returns a string for display by the caller:

<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>

Component (.razor) example (InvokeAsync)

TickerChanged calls the handleTickerChanged2 method and displays the returned string in the following CallJsExample4 component.

Pages/CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

Class (.cs) example (InvokeAsync)

JsInteropClasses2.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged calls the handleTickerChanged2 method and displays the returned string in the following CallJsExample5 component.

Pages/CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}

Dynamic content generation scenarios

For dynamic content generation with BuildRenderTree, use the [Inject] attribute:

[Inject]
IJSRuntime JS { get; set; }

Detect when a Blazor Server app is prerendering

This section applies to Blazor Server and hosted Blazor WebAssembly apps that prerender Razor components. Prerendering is covered in Prerender and integrate ASP.NET Core Razor components.

While an app is prerendering, certain actions, such as calling into JavaScript, aren't possible. Components may need to render differently when prerendered.

For the following example, the setElementText1 function is placed inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server). The function is called with JSRuntimeExtensions.InvokeVoidAsync and doesn't return a value:

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

Warning

The preceding example modifies the Document Object Model (DOM) directly for demonstration purposes only. Directly modifying the DOM with JavaScript isn't recommended in most scenarios because JavaScript can interfere with Blazor's change tracking. For more information, see Blazor JavaScript interoperability (JS interop).

To delay JavaScript interop calls until a point where such calls are guaranteed to work, override the OnAfterRender{Async} lifecycle event. This event is only called after the app is fully rendered.

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

Note

The preceding example pollutes the client with global methods. For a better approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

export setElementText1 = (element, text) => element.innerText = text;

The following component demonstrates how to use JavaScript interop as part of a component's initialization logic in a way that's compatible with prerendering. The component shows that it's possible to trigger a rendering update from inside OnAfterRenderAsync. The developer must be careful to avoid creating an infinite loop in this scenario.

For the following example, the setElementText2 function is placed inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server). The function is called withIJSRuntime.InvokeAsync and returns a value:

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

Warning

The preceding example modifies the Document Object Model (DOM) directly for demonstration purposes only. Directly modifying the DOM with JavaScript isn't recommended in most scenarios because JavaScript can interfere with Blazor's change tracking. For more information, see Blazor JavaScript interoperability (JS interop).

Where JSRuntime.InvokeAsync is called, the ElementReference is only used in OnAfterRenderAsync and not in any earlier lifecycle method because there's no JavaScript element until after the component is rendered.

StateHasChanged is called to rerender the component with the new state obtained from the JavaScript interop call (for more information, see ASP.NET Core Blazor component rendering). The code doesn't create an infinite loop because StateHasChanged is only called when data is null.

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string? data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

Note

The preceding example pollutes the client with global methods. For a better approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

Location of JavaScipt

Load JavaScript (JS) code using any of approaches described by the JavaScript (JS) interoperability (interop) overview article:

For information on isolating scripts in JS modules, see the JavaScript isolation in JavaScript modules section.

Warning

Don't place a <script> tag in a component file (.razor) because the <script> tag can't be updated dynamically.

JavaScript isolation in JavaScript modules

Blazor enables JavaScript (JS) isolation in standard JavaScript modules (ECMAScript specification).

JS isolation provides the following benefits:

  • Imported JS no longer pollutes the global namespace.
  • Consumers of a library and components aren't required to import the related JS.

For example, the following JS module exports a JS function for showing a browser window prompt. Place the following JS code in an external JS file.

wwwroot/scripts.js:

export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

Add the preceding JS module to an app or class library as a static web asset in the wwwroot folder and then import the module into the .NET code by calling InvokeAsync on the IJSRuntime instance.

IJSRuntime imports the module as an IJSObjectReference, which represents a reference to a JS object from .NET code. Use the IJSObjectReference to invoke exported JS functions from the module.

Pages/CallJsExample6.razor:

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference module;
    private string result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string> Prompt(string message)
    {
        return await module.InvokeAsync<string>("showPrompt", message);
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

In the preceding example:

  • By convention, the import identifier is a special identifier used specifically for importing a JS module.
  • Specify the module's external JS file using its stable static web asset path: ./{SCRIPT PATH AND FILENAME (.js)}, where:
    • The path segment for the current directory (./) is required in order to create the correct static asset path to the JS file.
    • The {SCRIPT PATH AND FILENAME (.js)} placeholder is the path and file name under wwwroot.

Dynamically importing a module requires a network request, so it can only be achieved asynchronously by calling InvokeAsync.

IJSInProcessObjectReference represents a reference to a JS object whose functions can be invoked synchronously.

Note

When the external JS file is supplied by a Razor class library, specify the module's JS file using its stable static web asset path: ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)}:

  • The path segment for the current directory (./) is required in order to create the correct static asset path to the JS file.
  • The {PACKAGE ID} placeholder is the library's package ID. The package ID defaults to the project's assembly name if <PackageId> isn't specified in the project file. In the following example, the library's assembly name is ComponentLibrary and the library's project file doesn't specify <PackageId>.
  • The {SCRIPT PATH AND FILENAME (.js)} placeholder is the path and file name under wwwroot. In the following example, the external JS file (script.js) is placed in the class library's wwwroot folder.
var module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

For more information, see Consume ASP.NET Core Razor components from Razor class libraries.

Capture references to elements

Some JavaScript (JS) interop scenarios require references to HTML elements. For example, a UI library may require an element reference for initialization, or you might need to call command-like APIs on an element, such as click or play.

Capture references to HTML elements in a component using the following approach:

  • Add an @ref attribute to the HTML element.
  • Define a field of type ElementReference whose name matches the value of the @ref attribute.

The following example shows capturing a reference to the username <input> element:

<input @ref="username" ... />

@code {
    private ElementReference username;
}

Warning

Only use an element reference to mutate the contents of an empty element that doesn't interact with Blazor. This scenario is useful when a third-party API supplies content to the element. Because Blazor doesn't interact with the element, there's no possibility of a conflict between Blazor's representation of the element and the Document Object Model (DOM).

In the following example, it's dangerous to mutate the contents of the unordered list (ul) because Blazor interacts with the DOM to populate this element's list items (<li>) from the Todos object:

<ul @ref="MyList">
    @foreach (var item in Todos)
    {
        <li>@item.Text</li>
    }
</ul>

If JS interop mutates the contents of element MyList and Blazor attempts to apply diffs to the element, the diffs won't match the DOM.

For more information, see Blazor JavaScript interoperability (JS interop).

An ElementReference is passed through to JS code via JS interop. The JS code receives an HTMLElement instance, which it can use with normal DOM APIs. For example, the following code defines a .NET extension method (TriggerClickEvent) that enables sending a mouse click to an element.

The JS function clickElement creates a click event on the passed HTML element (element):

window.interopFunctions = {
  clickElement : function (element) {
    element.click();
  }
}

To call a JS function that doesn't return a value, use JSRuntimeExtensions.InvokeVoidAsync. The following code triggers a client-side click event by calling the preceding JS function with the captured ElementReference:

@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await JS.InvokeVoidAsync(
            "interopFunctions.clickElement", exampleButton);
    }
}

To use an extension method, create a static extension method that receives the IJSRuntime instance:

public static async Task TriggerClickEvent(this ElementReference elementRef, 
    IJSRuntime js)
{
    await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}

The clickElement method is called directly on the object. The following example assumes that the TriggerClickEvent method is available from the JsInteropClasses namespace:

@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await exampleButton.TriggerClickEvent(JS);
    }
}

Important

The exampleButton variable is only populated after the component is rendered. If an unpopulated ElementReference is passed to JS code, the JS code receives a value of null. To manipulate element references after the component has finished rendering, use the OnAfterRenderAsync or OnAfterRender component lifecycle methods.

When working with generic types and returning a value, use ValueTask<TResult>:

public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef, 
    IJSRuntime js)
{
    return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}

The {JAVASCRIPT FUNCTION} placeholder is the JS function identifier.

GenericMethod is called directly on the object with a type. The following example assumes that the GenericMethod is available from the JsInteropClasses namespace:

@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}

Reference elements across components

An ElementReference can't be passed between components because:

For a parent component to make an element reference available to other components, the parent component can:

  • Allow child components to register callbacks.
  • Invoke the registered callbacks during the OnAfterRender event with the passed element reference. Indirectly, this approach allows child components to interact with the parent's element reference.

Add the following style to the <head> of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<style>
    .red { color: red }
</style>

Add the following script inside closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<script>
  function setElementClass(element, className) {
    var myElement = element;
    myElement.classList.add(className);
  }
</script>

Pages/CallJsExample7.razor (parent component):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="@this" Title="How is Blazor working for you?" />

Pages/CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}

In the preceding example, the namespace of the app is BlazorSample with components in the Pages folder. If testing the code locally, update the namespace.

Shared/SurveyPrompt.razor (child component):

@inject IJSRuntime JS

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string Title { get; set; }
}

Shared/SurveyPrompt.razor.cs:

using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}

In the preceding example, the namespace of the app is BlazorSample with shared components in the Shared folder. If testing the code locally, update the namespace.

Harden JavaScript interop calls

This section primarily applies to Blazor Server apps, but Blazor WebAssembly apps may also set JS interop timeouts if conditions warrant it.

In Blazor Server apps, JavaScript (JS) interop may fail due to networking errors and should be treated as unreliable. By default, Blazor Server apps use a one minute timeout for JS interop calls. If an app can tolerate a more aggressive timeout, set the timeout using one of the following approaches.

Set a global timeout in the Startup.ConfigureServices method of Startup.cs with CircuitOptions.JSInteropDefaultCallTimeout:

services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

The {TIMEOUT} placeholder is a TimeSpan (for example, TimeSpan.FromSeconds(80)).

Set a per-invocation timeout in component code. The specified timeout overrides the global timeout set by JSInteropDefaultCallTimeout:

var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1" });

In the preceding example:

  • The {TIMEOUT} placeholder is a TimeSpan (for example, TimeSpan.FromSeconds(80)).
  • The {ID} placeholder is the identifier for the function to invoke. For example, the value someScope.someFunction invokes the function window.someScope.someFunction.

Although a common cause of JS interop failures are network failures in Blazor Server apps, per-invocation timeouts can be set for JS interop calls in Blazor WebAssembly apps. Although no SignalR circuit exists in a Blazor WebAssembly app, JS interop calls might fail for other reasons that apply in Blazor WebAssembly apps.

For more information on resource exhaustion, see Threat mitigation guidance for ASP.NET Core Blazor Server.

Avoid circular object references

Objects that contain circular references can't be serialized on the client for either:

  • .NET method calls.
  • JavaScript method calls from C# when the return type has circular references.

JavaScript libraries that render UI

Sometimes you may wish to use JavaScript (JS) libraries that produce visible user interface elements within the browser Document Object Model (DOM). At first glance, this might seem difficult because Blazor's diffing system relies on having control over the tree of DOM elements and runs into errors if some external code mutates the DOM tree and invalidates its mechanism for applying diffs. This isn't a Blazor-specific limitation. The same challenge occurs with any diff-based UI framework.

Fortunately, it's straightforward to embed externally-generated UI within a Razor component UI reliably. The recommended technique is to have the component's code (.razor file) produce an empty element. As far as Blazor's diffing system is concerned, the element is always empty, so the renderer does not recurse into the element and instead leaves its contents alone. This makes it safe to populate the element with arbitrary externally-managed content.

The following example demonstrates the concept. Within the if statement when firstRender is true, interact with unmanagedElement outside of Blazor using JS interop. For example, call an external JS library to populate the element. Blazor leaves the element's contents alone until this component is removed. When the component is removed, the component's entire DOM subtree is also removed.

<h1>Hello! This is a Blazor component rendered at @DateTime.Now</h1>

<div @ref="unmanagedElement"></div>

@code {
    private HtmlElement unmanagedElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            ...
        }
    }
}

Consider the following example that renders an interactive map using open-source Mapbox APIs.

The following JS module is placed into the app or made available from a Razor class library.

Note

To create the Mapbox map, obtain an access token from Mapbox Sign in and provide it where the {ACCESS TOKEN} appears in the following code.

wwwroot/mapComponent.js:

import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';

mapboxgl.accessToken = '{ACCESS TOKEN}';

export function addMapToElement(element) {
  return new mapboxgl.Map({
    container: element,
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [-74.5, 40],
    zoom: 9
  });
}

export function setMapCenter(map, latitude, longitude) {
  map.setCenter([longitude, latitude]);
}

To produce correct styling, add the following stylesheet tag to the host HTML page.

In wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server), add the following <link> element to the <head> element markup:

<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
    rel="stylesheet" />

Pages/CallJsExample8.razor:

@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference mapModule;
    private IJSObjectReference mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
        => await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, latitude, 
            longitude).AsTask();

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}

The preceding example produces an interactive map UI. The user:

  • Can drag to scroll or zoom.
  • Select buttons to jump to predefined locations.

Mapbox street map of Tokyo, Japan with buttons to select Bristol, United Kingdom and Tokyo, Japan

In the preceding example:

  • The <div> with @ref="mapElement" is left empty as far as Blazor is concerned. The mapbox-gl.js script can safely populate the element and modify its contents. Use this technique with any JS library that renders UI. You can embed components from a third-party JS SPA framework inside Blazor components, as long as they don't try to reach out and modify other parts of the page. It is not safe for external JS code to modify elements that Blazor does not regard as empty.
  • When using this approach, bear in mind the rules about how Blazor retains or destroys DOM elements. The component safely handles button click events and updates the existing map instance because DOM elements are retained where possible by default. If you were rendering a list of map elements from inside a @foreach loop, you want to use @key to ensure the preservation of component instances. Otherwise, changes in the list data could cause component instances to retain the state of previous instances in an undesirable manner. For more information, see using @key to preserve elements and components.
  • The example encapsulates JS logic and dependencies within an ES6 module and loads the module dynamically using the import identifier. For more information, see JavaScript isolation in JavaScript modules.

Size limits on JavaScript interop calls

This section only applies to Blazor Server apps. In Blazor WebAssembly, the framework doesn't impose a limit on the size of JavaScript (JS) interop inputs and outputs.

In Blazor Server, JS interop calls are limited in size by the maximum incoming SignalR message size permitted for hub methods, which is enforced by HubOptions.MaximumReceiveMessageSize (default: 32 KB). JS to .NET SignalR messages larger than MaximumReceiveMessageSize throw an error. The framework doesn't impose a limit on the size of a SignalR message from the hub to a client.

When SignalR logging isn't set to Debug or Trace, a message size error only appears in the browser's developer tools console:

Error: Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'.

When SignalR server-side logging is set to Debug or Trace, server-side logging surfaces an InvalidDataException for a message size error.

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

Error:

System.IO.InvalidDataException: The maximum message size of 32768B was exceeded. The message size can be configured in AddHubOptions.

Increase the limit by setting MaximumReceiveMessageSize in Program.cs. The following example sets the maximum receive message size to 64 KB (64 * 1024) in ASP.NET Core 6.0 or later:

builder.Services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

In Startup.ConfigureServices for versions of ASP.NET Core earlier than 6.0:

services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

Increasing the SignalR incoming message size limit comes at the cost of requiring more server resources, and it exposes the server to increased risks from a malicious user. Additionally, reading a large amount of content in to memory as strings or byte arrays can also result in allocations that work poorly with the garbage collector, resulting in additional performance penalties.

One option for reading large payloads is to send the content in smaller chunks and process the payload as a Stream. This can be used when reading large JSON payloads or if data is available in JS as raw bytes. For an example that demonstrates sending large binary payloads in Blazor Server that uses techniques similar to the InputFile component, see the Binary Submit sample app.

Note

Documentation links to the ASP.NET Core reference source load the repository's main branch, which represents the product unit's current development for the next release of ASP.NET Core. To select the branch for a different release, use the Switch branches or tags dropdown list to select the branch. For example, select the release/5.0 branch for the ASP.NET Core 5.0 release.

Consider the following guidance when developing code that transfers a large amount of data between JS and Blazor in Blazor Server apps:

  • Slice the data into smaller pieces, and send the data segments sequentially until all of the data is received by the server.
  • Don't allocate large objects in JS and C# code.
  • Don't block the main UI thread for long periods when sending or receiving data.
  • Free consumed memory when the process is completed or cancelled.
  • Enforce the following additional requirements for security purposes:
    • Declare the maximum file or data size that can be passed.
    • Declare the minimum upload rate from the client to the server.
  • After the data is received by the server, the data can be:
    • Temporarily stored in a memory buffer until all of the segments are collected.
    • Consumed immediately. For example, the data can be stored immediately in a database or written to disk as each segment is received.

Unmarshalled JavaScript interop

Blazor WebAssembly components may experience poor performance when .NET objects are serialized for JavaScript (JS) interop and either of the following are true:

  • A high volume of .NET objects are rapidly serialized. For example, poor performance may result when JS interop calls are made on the basis of moving an input device, such as spinning a mouse wheel.
  • Large .NET objects or many .NET objects must be serialized for JS interop. For example, poor performance may result when JS interop calls require serializing dozens of files.

IJSUnmarshalledObjectReference represents a reference to an JS object whose functions can be invoked without the overhead of serializing .NET data.

In the following example:

  • A struct containing a string and an integer is passed unserialized to JS.
  • JS functions process the data and return either a boolean or string to the caller.
  • A JS string isn't directly convertible into a .NET string object. The unmarshalledFunctionReturnString function calls BINDING.js_string_to_mono_string to manage the conversion of a JS string.

Note

The following examples aren't typical use cases for this scenario because the struct passed to JS doesn't result in poor component performance. The example uses a small object merely to demonstrate the concepts for passing unserialized .NET data.

Place the following <script> block in wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server). Alternatively, you can place the JS in an external JS file referenced inside the closing </body> tag with <script src="{SCRIPT PATH AND FILE NAME (.js)}></script>, where the {SCRIPT PATH AND FILE NAME (.js)} placeholder is the path and file name of the script.

<script>
  window.returnObjectReference = () => {
    return {
      unmarshalledFunctionReturnBoolean: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return name === "Brigadier Alistair Gordon Lethbridge-Stewart" &&
            year === 1968;
      },
      unmarshalledFunctionReturnString: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return BINDING.js_string_to_mono_string(`Hello, ${name} (${year})!`);
      }
    };
  }
</script>

Warning

The js_string_to_mono_string function name, behavior, and existence is subject to change in a future release of .NET. For example:

  • The function is likely to be renamed.
  • The function itself might be removed in favor of automatic conversion of strings by the framework.

Pages/CallJsExample10.razor:

@page "/call-js-example-10"
@using System.Runtime.InteropServices
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Call JS Example 10</h1>

@if (callResultForBoolean)
{
    <p>JS interop was successful!</p>
}

@if (!string.IsNullOrEmpty(callResultForString))
{
    <p>@callResultForString</p>
}

<p>
    <button @onclick="CallJSUnmarshalledForBoolean">
        Call Unmarshalled JS & Return Boolean
    </button>
    <button @onclick="CallJSUnmarshalledForString">
        Call Unmarshalled JS & Return String
    </button>
</p>

<p>
    <a href="https://www.doctorwho.tv">Doctor Who</a>
    is a registered trademark of the <a href="https://www.bbc.com/">BBC</a>.
</p>

@code {
    private bool callResultForBoolean;
    private string callResultForString;

    private void CallJSUnmarshalledForBoolean()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForBoolean = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>(
                "unmarshalledFunctionReturnBoolean", GetStruct());
    }

    private void CallJSUnmarshalledForString()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForString = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>(
                "unmarshalledFunctionReturnString", GetStruct());
    }

    private InteropStruct GetStruct()
    {
        return new InteropStruct
        {
            Name = "Brigadier Alistair Gordon Lethbridge-Stewart",
            Year = 1968,
        };
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InteropStruct
    {
        [FieldOffset(0)]
        public string Name;

        [FieldOffset(8)]
        public int Year;
    }
}

If an IJSUnmarshalledObjectReference instance isn't disposed in C# code, it can be disposed in JS. The following dispose function disposes the object reference when called from JS:

window.exampleJSObjectReferenceNotDisposedInCSharp = () => {
  return {
    dispose: function () {
      DotNet.disposeJSObjectReference(this);
    },

    ...
  };
}

Array types can be converted from JS objects into .NET objects using js_typed_array_to_array, but the JS array must be a typed array. Arrays from JS can be read in C# code as a .NET object array (object[]).

Other data types, such as string arrays, can be converted but require creating a new Mono array object (mono_obj_array_new) and setting its value (mono_obj_array_set).

Warning

JS functions provided by the Blazor framework, such as js_typed_array_to_array, mono_obj_array_new, and mono_obj_array_set, are subject to name changes, behavioral changes, or removal in future releases of .NET.

Catch JavaScript exceptions

To catch JS exceptions, wrap the JS interop in a try-catch block and catch a JSException.

In the following example, the nonFunction JS function doesn't exist. When the function isn't found, the JSException is trapped with a Message that indicates the following error:

Could not find 'nonFunction' ('nonFunction' was undefined).

Pages/CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string errorMessage;
    private string result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

Additional resources

This article covers invoking JavaScript (JS) functions from .NET. For information on how to call .NET methods from JS, see Call .NET methods from JavaScript functions in ASP.NET Core Blazor.

To call into JS from .NET, inject the IJSRuntime abstraction and call one of the following methods:

For the preceding .NET methods that invoke JS functions:

  • The function identifier (String) is relative to the global scope (window). To call window.someScope.someFunction, the identifier is someScope.someFunction. There's no need to register the function before it's called.
  • Pass any number of JSON-serializable arguments in Object[] to a JS function.
  • The cancellation token (CancellationToken) propagates a notification that operations should be canceled.
  • TimeSpan represents a time limit for a JS operation.
  • The TValue return type must also be JSON serializable. TValue should match the .NET type that best maps to the JSON type returned.
  • A JS Promise is returned for InvokeAsync methods. InvokeAsync unwraps the Promise and returns the value awaited by the Promise.

For Blazor Server apps with prerendering enabled, calling into JS isn't possible during initial prerendering. JS interop calls must be deferred until after the connection with the browser is established. For more information, see the Detect when a Blazor Server app is prerendering section.

The following example is based on TextDecoder, a JS-based decoder. The example demonstrates how to invoke a JS function from a C# method that offloads a requirement from developer code to an existing JS API. The JS function accepts a byte array from a C# method, decodes the array, and returns the text to the component for display.

Add the following JS code inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    console.log(decodedArray);
    return decodedArray;
  };
</script>

The following CallJsExample1 component:

  • Invokes the convertArray JS function with InvokeAsync when selecting a button (Convert Array).
  • After the JS function is called, the passed array is converted into a string. The string is returned to the component for display (text).

Pages/CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new MarkupString(await JS.InvokeAsync<string>("convertArray", 
            quoteArray));
    }
}

Invoke JavaScript functions without reading a returned value (InvokeVoidAsync)

Use InvokeVoidAsync when:

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server), provide a displayTickerAlert1 JS function. The function is called with InvokeVoidAsync and doesn't return a value:

<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>

Component (.razor) example (InvokeVoidAsync)

TickerChanged calls the handleTickerChanged1 method in the following CallJsExample2 component.

Pages/CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

Class (.cs) example (InvokeVoidAsync)

JsInteropClasses1.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged calls the handleTickerChanged1 method in the following CallJsExample3 component.

Pages/CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses1(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}

Invoke JavaScript functions and read a returned value (InvokeAsync)

Use InvokeAsync when .NET should read the result of a JS call.

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server), provide a displayTickerAlert2 JS function. The following example returns a string for display by the caller:

<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>

Component (.razor) example (InvokeAsync)

TickerChanged calls the handleTickerChanged2 method and displays the returned string in the following CallJsExample4 component.

Pages/CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

Class (.cs) example (InvokeAsync)

JsInteropClasses2.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged calls the handleTickerChanged2 method and displays the returned string in the following CallJsExample5 component.

Pages/CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses2(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}

Dynamic content generation scenarios

For dynamic content generation with BuildRenderTree, use the [Inject] attribute:

[Inject]
IJSRuntime JS { get; set; }

Detect when a Blazor Server app is prerendering

This section applies to Blazor Server and hosted Blazor WebAssembly apps that prerender Razor components. Prerendering is covered in Prerender and integrate ASP.NET Core Razor components.

While an app is prerendering, certain actions, such as calling into JavaScript, aren't possible. Components may need to render differently when prerendered.

For the following example, the setElementText1 function is placed inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server). The function is called with JSRuntimeExtensions.InvokeVoidAsync and doesn't return a value:

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

Warning

The preceding example modifies the Document Object Model (DOM) directly for demonstration purposes only. Directly modifying the DOM with JavaScript isn't recommended in most scenarios because JavaScript can interfere with Blazor's change tracking. For more information, see Blazor JavaScript interoperability (JS interop).

To delay JavaScript interop calls until a point where such calls are guaranteed to work, override the OnAfterRender{Async} lifecycle event. This event is only called after the app is fully rendered.

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

Note

The preceding example pollutes the client with global methods. For a better approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

export setElementText1 = (element, text) => element.innerText = text;

The following component demonstrates how to use JavaScript interop as part of a component's initialization logic in a way that's compatible with prerendering. The component shows that it's possible to trigger a rendering update from inside OnAfterRenderAsync. The developer must be careful to avoid creating an infinite loop in this scenario.

For the following example, the setElementText2 function is placed inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Layout.cshtml (Blazor Server). The function is called withIJSRuntime.InvokeAsync and returns a value:

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

Warning

The preceding example modifies the Document Object Model (DOM) directly for demonstration purposes only. Directly modifying the DOM with JavaScript isn't recommended in most scenarios because JavaScript can interfere with Blazor's change tracking. For more information, see Blazor JavaScript interoperability (JS interop).

Where JSRuntime.InvokeAsync is called, the ElementReference is only used in OnAfterRenderAsync and not in any earlier lifecycle method because there's no JavaScript element until after the component is rendered.

StateHasChanged is called to rerender the component with the new state obtained from the JavaScript interop call (for more information, see ASP.NET Core Blazor component rendering). The code doesn't create an infinite loop because StateHasChanged is only called when data is null.

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string? data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

Note

The preceding example pollutes the client with global methods. For a better approach in production apps, see JavaScript isolation in JavaScript modules.

Example:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

Location of JavaScipt

Load JavaScript (JS) code using any of approaches described by the JavaScript (JS) interoperability (interop) overview article:

Warning

Don't place a <script> tag in a component file (.razor) because the <script> tag can't be updated dynamically.

Capture references to elements

Some JavaScript (JS) interop scenarios require references to HTML elements. For example, a UI library may require an element reference for initialization, or you might need to call command-like APIs on an element, such as click or play.

Capture references to HTML elements in a component using the following approach:

  • Add an @ref attribute to the HTML element.
  • Define a field of type ElementReference whose name matches the value of the @ref attribute.

The following example shows capturing a reference to the username <input> element:

<input @ref="username" ... />

@code {
    private ElementReference username;
}

Warning

Only use an element reference to mutate the contents of an empty element that doesn't interact with Blazor. This scenario is useful when a third-party API supplies content to the element. Because Blazor doesn't interact with the element, there's no possibility of a conflict between Blazor's representation of the element and the Document Object Model (DOM).

In the following example, it's dangerous to mutate the contents of the unordered list (ul) because Blazor interacts with the DOM to populate this element's list items (<li>) from the Todos object:

<ul @ref="MyList">
    @foreach (var item in Todos)
    {
        <li>@item.Text</li>
    }
</ul>

If JS interop mutates the contents of element MyList and Blazor attempts to apply diffs to the element, the diffs won't match the DOM.

For more information, see Blazor JavaScript interoperability (JS interop).

An ElementReference is passed through to JS code via JS interop. The JS code receives an HTMLElement instance, which it can use with normal DOM APIs. For example, the following code defines a .NET extension method (TriggerClickEvent) that enables sending a mouse click to an element.

The JS function clickElement creates a click event on the passed HTML element (element):

window.interopFunctions = {
  clickElement : function (element) {
    element.click();
  }
}

To call a JS function that doesn't return a value, use JSRuntimeExtensions.InvokeVoidAsync. The following code triggers a client-side click event by calling the preceding JS function with the captured ElementReference:

@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await JS.InvokeVoidAsync(
            "interopFunctions.clickElement", exampleButton);
    }
}

To use an extension method, create a static extension method that receives the IJSRuntime instance:

public static async Task TriggerClickEvent(this ElementReference elementRef, 
    IJSRuntime js)
{
    await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}

The clickElement method is called directly on the object. The following example assumes that the TriggerClickEvent method is available from the JsInteropClasses namespace:

@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await exampleButton.TriggerClickEvent(JS);
    }
}

Important

The exampleButton variable is only populated after the component is rendered. If an unpopulated ElementReference is passed to JS code, the JS code receives a value of null. To manipulate element references after the component has finished rendering, use the OnAfterRenderAsync or OnAfterRender component lifecycle methods.

When working with generic types and returning a value, use ValueTask<TResult>:

public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef, 
    IJSRuntime js)
{
    return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}

The {JAVASCRIPT FUNCTION} placeholder is the JS function identifier.

GenericMethod is called directly on the object with a type. The following example assumes that the GenericMethod is available from the JsInteropClasses namespace:

@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}

Reference elements across components

An ElementReference can't be passed between components because:

For a parent component to make an element reference available to other components, the parent component can:

  • Allow child components to register callbacks.
  • Invoke the registered callbacks during the OnAfterRender event with the passed element reference. Indirectly, this approach allows child components to interact with the parent's element reference.

Add the following style to the <head> of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<style>
    .red { color: red }
</style>

Add the following script inside closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<script>
  function setElementClass(element, className) {
    var myElement = element;
    myElement.classList.add(className);
  }
</script>

Pages/CallJsExample7.razor (parent component):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="@this" Title="How is Blazor working for you?" />

Pages/CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}

In the preceding example, the namespace of the app is BlazorSample with components in the Pages folder. If testing the code locally, update the namespace.

Shared/SurveyPrompt.razor (child component):

@inject IJSRuntime JS

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string Title { get; set; }
}

Shared/SurveyPrompt.razor.cs:

using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}

In the preceding example, the namespace of the app is BlazorSample with shared components in the Shared folder. If testing the code locally, update the namespace.

Harden JavaScript interop calls

This section primarily applies to Blazor Server apps, but Blazor WebAssembly apps may also set JS interop timeouts if conditions warrant it.

In Blazor Server apps, JavaScript (JS) interop may fail due to networking errors and should be treated as unreliable. By default, Blazor Server apps use a one minute timeout for JS interop calls. If an app can tolerate a more aggressive timeout, set the timeout using one of the following approaches.

Set a global timeout in the Startup.ConfigureServices method of Startup.cs with CircuitOptions.JSInteropDefaultCallTimeout:

services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

The {TIMEOUT} placeholder is a TimeSpan (for example, TimeSpan.FromSeconds(80)).

Set a per-invocation timeout in component code. The specified timeout overrides the global timeout set by JSInteropDefaultCallTimeout:

var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1" });

In the preceding example:

  • The {TIMEOUT} placeholder is a TimeSpan (for example, TimeSpan.FromSeconds(80)).
  • The {ID} placeholder is the identifier for the function to invoke. For example, the value someScope.someFunction invokes the function window.someScope.someFunction.

Although a common cause of JS interop failures are network failures in Blazor Server apps, per-invocation timeouts can be set for JS interop calls in Blazor WebAssembly apps. Although no SignalR circuit exists in a Blazor WebAssembly app, JS interop calls might fail for other reasons that apply in Blazor WebAssembly apps.

For more information on resource exhaustion, see Threat mitigation guidance for ASP.NET Core Blazor Server.

Avoid circular object references

Objects that contain circular references can't be serialized on the client for either:

  • .NET method calls.
  • JavaScript method calls from C# when the return type has circular references.

Size limits on JavaScript interop calls

This section only applies to Blazor Server apps. In Blazor WebAssembly, the framework doesn't impose a limit on the size of JavaScript (JS) interop inputs and outputs.

In Blazor Server, JS interop calls are limited in size by the maximum incoming SignalR message size permitted for hub methods, which is enforced by HubOptions.MaximumReceiveMessageSize (default: 32 KB). JS to .NET SignalR messages larger than MaximumReceiveMessageSize throw an error. The framework doesn't impose a limit on the size of a SignalR message from the hub to a client.

When SignalR logging isn't set to Debug or Trace, a message size error only appears in the browser's developer tools console:

Error: Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'.

When SignalR server-side logging is set to Debug or Trace, server-side logging surfaces an InvalidDataException for a message size error.

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

Error:

System.IO.InvalidDataException: The maximum message size of 32768B was exceeded. The message size can be configured in AddHubOptions.

Increase the limit by setting MaximumReceiveMessageSize in Program.cs. The following example sets the maximum receive message size to 64 KB (64 * 1024) in ASP.NET Core 6.0 or later:

builder.Services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

In Startup.ConfigureServices for versions of ASP.NET Core earlier than 6.0:

services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

Increasing the SignalR incoming message size limit comes at the cost of requiring more server resources, and it exposes the server to increased risks from a malicious user. Additionally, reading a large amount of content in to memory as strings or byte arrays can also result in allocations that work poorly with the garbage collector, resulting in additional performance penalties.

One option for reading large payloads is to send the content in smaller chunks and process the payload as a Stream. This can be used when reading large JSON payloads or if data is available in JS as raw bytes. For an example that demonstrates sending large binary payloads in Blazor Server that uses techniques similar to the InputFile component, see the Binary Submit sample app.

Note

Documentation links to the ASP.NET Core reference source load the repository's main branch, which represents the product unit's current development for the next release of ASP.NET Core. To select the branch for a different release, use the Switch branches or tags dropdown list to select the branch. For example, select the release/5.0 branch for the ASP.NET Core 5.0 release.

Consider the following guidance when developing code that transfers a large amount of data between JS and Blazor in Blazor Server apps:

  • Slice the data into smaller pieces, and send the data segments sequentially until all of the data is received by the server.
  • Don't allocate large objects in JS and C# code.
  • Don't block the main UI thread for long periods when sending or receiving data.
  • Free consumed memory when the process is completed or cancelled.
  • Enforce the following additional requirements for security purposes:
    • Declare the maximum file or data size that can be passed.
    • Declare the minimum upload rate from the client to the server.
  • After the data is received by the server, the data can be:
    • Temporarily stored in a memory buffer until all of the segments are collected.
    • Consumed immediately. For example, the data can be stored immediately in a database or written to disk as each segment is received.

Catch JavaScript exceptions

To catch JS exceptions, wrap the JS interop in a try-catch block and catch a JSException.

In the following example, the nonFunction JS function doesn't exist. When the function isn't found, the JSException is trapped with a Message that indicates the following error:

Could not find 'nonFunction' ('nonFunction' was undefined).

Pages/CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string errorMessage;
    private string result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

Additional resources