blazor-js-interop-call-js-from-csharp

JS Interop: Calling JavaScript from C#

  • 4 min

The interoperability with JavaScript is the mechanism by which .NET code and the JavaScript environment exchange calls and data.

Blazor allows you to write much of the interface in C#, but we still need JavaScript for some browser APIs and to integrate libraries like Chart.js.

Blazor does not isolate your application in a bubble; on the contrary, it allows you to interact with the web ecosystem through the mechanism known as JS Interop.

The IJSRuntime service

The gateway to JavaScript is an abstraction: the IJSRuntime interface. Like any other service in Blazor, we must inject it into our component.

@inject IJSRuntime JS
Copied!

This service is responsible for serializing your data into JSON, sending it to the JavaScript environment (browser), executing the function you request, and returning the deserialized result.

Defining the JavaScript function

In this first example, the function will be globally accessible from window. Later, we will move it to a module to avoid polluting the global scope.

For a demonstration, you can load this script from the root document. In real applications, external files and JS modules are recommended, not <script> tags inside interactive components.

Let’s add a simple script in our base HTML:

<script>
    window.utilityFunctions = {
        showAlert: function (message) {
            alert(message);
        },
        requestConfirmation: function (message) {
            return confirm(message); // Returns true or false
        },
        addNumbers: function (a, b) {
            return a + b;
        }
    };
</script>
Copied!

Calling from C#

The IJSRuntime service provides us with two main methods, both asynchronous.

InvokeVoidAsync for calls without a result

We use this method when we want to execute an action in JS and do not expect any return value (or we don’t care about it). It is ideal for side effects like logs, alerts, or DOM manipulation.

<button @onclick="LaunchAlert">Greet</button>

@code {
    private async Task LaunchAlert()
    {
        // 1st parameter: Name of the JS function
        // 2nd parameter: Argument(s) for the function
        await JS.InvokeVoidAsync("utilityFunctions.showAlert", "Hello from C#!");
    }
}
Copied!

Even if the JS function returns nothing, on Blazor Server it is important to use await. The call travels over the network (SignalR) to the browser, executes, and returns the confirmation. If you don’t await, you could have race conditions.

InvokeAsync<T> to get a result

We use this method when the JS function returns a value that we need in C#. Blazor will automatically deserialize the result to the type T we specify.

<button @onclick="ConfirmDeletion">Delete File</button>
<p>Result: @message</p>

@code {
    private string message = string.Empty;

    private async Task ConfirmDeletion()
    {
        // We expect a boolean back
        bool confirmed = await JS.InvokeAsync<bool>(
            "utilityFunctions.requestConfirmation",
            "Are you sure you want to delete this?");

        if (confirmed)
        {
            message = "File deleted ✅";
            // Deletion logic...
        }
        else
        {
            message = "Operation cancelled ❌";
        }
    }
}
Copied!

Blazor handles primitive types (int, string, bool) and complex objects (DTOs) without issue, as long as they are serializable to JSON.

The right time to access the DOM

During prerendering, you cannot call JavaScript. Furthermore, a reference to an element does not yet exist in OnInitialized because the browser hasn’t rendered that element yet.

To initialize a library on the DOM, wait for OnAfterRenderAsync. Calls that do not depend on the DOM can also be made from an interactive event handler.

@inject IJSRuntime JS

<input @ref="myInput" placeholder="Auto focus..." />

@code {
    private ElementReference myInput;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // We only want to set focus the first time the page loads
        if (firstRender)
        {
            await JS.InvokeVoidAsync("utilityFunctions.focus", myInput);
        }
    }
}
Copied!

The JavaScript function receives the element reference:

window.utilityFunctions.focus = element => element.focus();
Copied!

Encapsulating calls

Filling your components with JS.InvokeAsync calls is a bad practice. It clutters the code and makes it hard to test.

The “Luis Llamas” recommendation is to encapsulate these calls in a C# service.

Bad practice:

// In the component
await JS.InvokeVoidAsync("localStorage.setItem", "key", "value");
Copied!

Good practice: Create an ILocalStorageService.

public class LocalStorageService
{
    private readonly IJSRuntime _js;

    public LocalStorageService(IJSRuntime js) => _js = js;

    public async Task SetItemAsync(string key, string value)
    {
        await _js.InvokeVoidAsync("localStorage.setItem", key, value);
    }
}
Copied!

This way, your component only injects ILocalStorageService and knows nothing about JavaScript. Moreover, if tomorrow you change LocalStorage for IndexedDB, the component won’t even know.