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

By Javier Calvarro Nelson, Daniel Roth, and Luke Latham

A Blazor app can invoke JavaScript functions from .NET methods and .NET methods from JavaScript functions. These scenarios are called JavaScript interoperability (JS interop).

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

View or download sample code (how to download)

To call into JavaScript from .NET, use the IJSRuntime abstraction. To issue JS interop calls, inject the IJSRuntime abstraction in your component. InvokeAsync takes an identifier for the JavaScript function that you wish to invoke along with any number of JSON-serializable arguments. The function identifier is relative to the global scope (window). If you wish to call window.someScope.someFunction, the identifier is someScope.someFunction. There's no need to register the function before it's called. The return type T must also be JSON serializable. T should match the .NET type that best maps to the JSON type returned.

For Blazor Server apps with prerendering enabled, calling into JavaScript isn't possible during the initial prerendering. JavaScript 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 JavaScript-based decoder. The example demonstrates how to invoke a JavaScript function from a C# method that offloads a requirement from developer code to an existing JavaScript API. The JavaScript function accepts a byte array from a C# method, decodes the array, and returns the text to the component for display.

Inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server), provide a JavaScript function that uses TextDecoder to decode a passed array and return the decoded value:

<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>

JavaScript code, such as the code shown in the preceding example, can also be loaded from a JavaScript file (.js) with a reference to the script file:

<script src="exampleJsInterop.js"></script>

The following component:

  • Invokes the convertArray JavaScript function using JSRuntime when a component button (Convert Array) is selected.
  • After the JavaScript function is called, the passed array is converted into a string. The string is returned to the component for display.
@page "/call-js-example"
@inject IJSRuntime JSRuntime;

<h1>Call JavaScript Function Example</h1>

<button type="button" class="btn btn-primary" @onclick="ConvertArray">
    Convert Array
</button>

<p class="mt-2" style="font-size:1.6em">
    <span class="badge badge-success">
        @convertedText
    </span>
</p>

@code {
    // Quote (c)2005 Universal Pictures: Serenity
    // https://www.uphe.com/movies/serenity
    // David Krumholtz on IMDB: https://www.imdb.com/name/nm0472710/

    private MarkupString convertedText =
        new MarkupString("Select the <b>Convert Array</b> button.");

    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()
    {
        var text =
            await JSRuntime.InvokeAsync<string>("convertArray", quoteArray);

        convertedText = new MarkupString(text);
    }
}

IJSRuntime

To use the IJSRuntime abstraction, adopt any of the following approaches:

  • Inject the IJSRuntime abstraction into the Razor component (.razor):

    @inject IJSRuntime JSRuntime
    
    @code {
        protected override void OnInitialized()
        {
            StocksService.OnStockTickerUpdated += stockUpdate =>
            {
                JSRuntime.InvokeVoidAsync("handleTickerChanged",
                    stockUpdate.symbol, stockUpdate.price);
            };
        }
    }
    

    Inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server), provide a handleTickerChanged JavaScript function. The function is called with JSRuntimeExtensions.InvokeVoidAsync and doesn't return a value:

    <script>
      window.handleTickerChanged = (symbol, price) => {
        // ... client-side processing/display code ...
      };
    </script>
    
  • Inject the IJSRuntime abstraction into a class (.cs):

    public class JsInteropClasses
    {
        private readonly IJSRuntime jsRuntime;
    
        public JsInteropClasses(IJSRuntime jsRuntime)
        {
            this.jsRuntime = jsRuntime;
        }
    
        public ValueTask<string> TickerChanged(string data)
        {
            return jsRuntime.InvokeAsync<string>(
                "handleTickerChanged",
                stockUpdate.symbol,
                stockUpdate.price);
        }
    }
    

    Inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server), provide a handleTickerChanged JavaScript function. The function is called with JSRuntime.InvokeAsync and returns a value:

    <script>
      window.handleTickerChanged = (symbol, price) => {
        // ... client-side processing/display code ...
        return 'Done!';
      };
    </script>
    
  • For dynamic content generation with BuildRenderTree, use the [Inject] attribute:

    [Inject]
    IJSRuntime JSRuntime { get; set; }
    

In the client-side sample app that accompanies this topic, two JavaScript functions are available to the app that interact with the DOM to receive user input and display a welcome message:

  • showPrompt: Produces a prompt to accept user input (the user's name) and returns the name to the caller.
  • displayWelcome: Assigns a welcome message from the caller to a DOM object with an id of welcome.

wwwroot/exampleJsInterop.js:

window.exampleJsFunctions = {
  showPrompt: function (text) {
    return prompt(text, 'Type your name here');
  },
  displayWelcome: function (welcomeMessage) {
    document.getElementById('welcome').innerText = welcomeMessage;
  },
  returnArrayAsyncJs: function () {
    DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
      .then(data => {
        data.push(4);
          console.log(data);
      });
  },
  sayHello: function (dotnetHelper) {
    return dotnetHelper.invokeMethodAsync('SayHello')
      .then(r => console.log(r));
  }
};

Place the <script> tag that references the JavaScript file in the wwwroot/index.html file (Blazor WebAssembly) or Pages/_Host.cshtml file (Blazor Server).

wwwroot/index.html (Blazor WebAssembly):

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Blazor WebAssembly Sample</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
</head>

<body>
    <app>Loading...</app>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="exampleJsInterop.js"></script>
</body>

</html>

Pages/_Host.cshtml (Blazor Server):

@page "/"
@namespace BlazorSample.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Blazor Server Sample</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>
        <component type="typeof(App)" render-mode="ServerPrerendered" />
    </app>

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.server.js"></script>
    <script src="exampleJsInterop.js"></script>
</body>
</html>

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

.NET methods interop with the JavaScript functions in the exampleJsInterop.js file by calling IJSRuntime.InvokeAsync.

The IJSRuntime abstraction is asynchronous to allow for Blazor Server scenarios. If the app is a Blazor WebAssembly app and you want to invoke a JavaScript function synchronously, downcast to IJSInProcessRuntime and call Invoke instead. We recommend that most JS interop libraries use the async APIs to ensure that the libraries are available in all scenarios.

The sample app includes a component to demonstrate JS interop. The component:

  • Receives user input via a JavaScript prompt.
  • Returns the text to the component for processing.
  • Calls a second JavaScript function that interacts with the DOM to display a welcome message.

Pages/JsInterop.razor:

@page "/JSInterop"
@using BlazorSample.JsInteropClasses
@inject IJSRuntime JSRuntime

<h1>JavaScript Interop</h1>

<h2>Invoke JavaScript functions from .NET methods</h2>

<button type="button" class="btn btn-primary" @onclick="TriggerJsPrompt">
    Trigger JavaScript Prompt
</button>

<h3 id="welcome" style="color:green;font-style:italic"></h3>

@code {
    public async Task TriggerJsPrompt()
    {
        var name = await JSRuntime.InvokeAsync<string>(
                "exampleJsFunctions.showPrompt",
                "What's your name?");

        await JSRuntime.InvokeVoidAsync(
                "exampleJsFunctions.displayWelcome",
                $"Hello {name}! Welcome to Blazor!");
    }
}
  1. When TriggerJsPrompt is executed by selecting the component's Trigger JavaScript Prompt button, the JavaScript showPrompt function provided in the wwwroot/exampleJsInterop.js file is called.
  2. The showPrompt function accepts user input (the user's name), which is HTML-encoded and returned to the component. The component stores the user's name in a local variable, name.
  3. The string stored in name is incorporated into a welcome message, which is passed to a JavaScript function, displayWelcome, which renders the welcome message into a heading tag.

Call a void JavaScript function

JavaScript functions that return void(0)/void 0 or undefined are called with JSRuntimeExtensions.InvokeVoidAsync.

Detect when a Blazor Server app is prerendering



While a Blazor Server app is prerendering, certain actions, such as calling into JavaScript, aren't possible because a connection with the browser hasn't been established. Components may need to render differently when prerendered.

To delay JavaScript interop calls until after the connection with the browser is established, you can use the OnAfterRenderAsync component lifecycle event. This event is only called after the app is fully rendered and the client connection is established.

@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime

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

@code {
    private ElementReference divElement;

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

For the preceding example code, provide a setElementText JavaScript function inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server). The function is called with JSRuntimeExtensions.InvokeVoidAsync and doesn't return a value:

<script>
  window.setElementText = (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.

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 avoid creating an infinite loop in this scenario.

Where JSRuntime.InvokeAsync is called, ElementRef 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. The code doesn't create an infinite loop because StateHasChanged is only called when infoFromJs is null.

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

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

Set value via JS interop call:
<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string infoFromJs;
    private ElementReference divElement;

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

            StateHasChanged();
        }
    }
}

For the preceding example code, provide a setElementText JavaScript function inside the <head> element of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server). The function is called withIJSRuntime.InvokeAsync and returns a value:

<script>
  window.setElementText = (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.

Capture references to elements

Some 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 focus 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 {
    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 3rd 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 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>):

<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.

As far as .NET code is concerned, an ElementReference is an opaque handle. The only thing you can do with ElementReference is pass it through to JavaScript code via JS interop. When you do so, the JavaScript-side code receives an HTMLElement instance, which it can use with normal DOM APIs.

For example, the following code defines a .NET extension method that enables setting the focus on an element:

exampleJsInterop.js:

window.exampleJsFunctions = {
  focusElement : function (element) {
    element.focus();
  }
}

To call a JavaScript function that doesn't return a value, use JSRuntimeExtensions.InvokeVoidAsync. The following code sets the focus on the username input by calling the preceding JavaScript function with the captured ElementReference:

@inject IJSRuntime JSRuntime

<input @ref="username" />
<button @onclick="SetFocus">Set focus on username</button>

@code {
    private ElementReference username;

    public async Task SetFocus()
    {
        await JSRuntime.InvokeVoidAsync(
            "exampleJsFunctions.focusElement", username);
    }
}

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

public static async Task Focus(this ElementReference elementRef, IJSRuntime jsRuntime)
{
    await jsRuntime.InvokeVoidAsync(
        "exampleJsFunctions.focusElement", elementRef);
}

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

@inject IJSRuntime JSRuntime
@using JsInteropClasses

<input @ref="username" />
<button @onclick="SetFocus">Set focus on username</button>

@code {
    private ElementReference username;

    public async Task SetFocus()
    {
        await username.Focus(JSRuntime);
    }
}

Important

The username variable is only populated after the component is rendered. If an unpopulated ElementReference is passed to JavaScript code, the JavaScript code receives a value of null. To manipulate element references after the component has finished rendering (to set the initial focus on an element) 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 jsRuntime)
{
    return jsRuntime.InvokeAsync<T>(
        "exampleJsFunctions.doSomethingGeneric", elementRef);
}

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 JSRuntime
@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>(JSRuntime);
    }
}

Reference elements across components

An ElementReference is only guaranteed valid in a component's OnAfterRender method (and an element reference is a struct), so an element reference can't be passed between components.

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.

The following Blazor WebAssembly example illustrates the approach.

In the <head> of wwwroot/index.html:

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

In the <body> of wwwroot/index.html:

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

Pages/Index.razor (parent component):

@page "/"

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

Welcome to your new app.

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

Pages/Index.razor.cs:

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

namespace BlazorSample.Pages
{
    public partial class Index : 
        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, Index self)
            {
                Observer = observer;
                Self = self;
            }

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

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

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();

            if (subscription != null)
            {
                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();
        }
    }
}

Harden JS interop calls

JS interop may fail due to networking errors and should be treated as unreliable. By default, a Blazor Server app times out JS interop calls on the server after one minute. If an app can tolerate a more aggressive timeout, set the timeout using one of the following approaches:

  • Globally in Startup.ConfigureServices, specify the timeout:

    services.AddServerSideBlazor(
        options => options.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds({SECONDS}));
    
  • Per-invocation in component code, a single call can specify the timeout:

    var result = await JSRuntime.InvokeAsync<string>("MyJSOperation", 
        TimeSpan.FromSeconds({SECONDS}), new[] { "Arg1" });
    

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

Share interop code in a class library

JS interop code can be included in a class library, which allows you to share the code in a NuGet package.

The class library handles embedding JavaScript resources in the built assembly. The JavaScript files are placed in the wwwroot folder. The tooling takes care of embedding the resources when the library is built.

The built NuGet package is referenced in the app's project file the same way that any NuGet package is referenced. After the package is restored, app code can call into JavaScript as if it were C#.

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

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.

For more information, see the following issues:

Additional resources