ASP.NET Core Blazor JavaScript interoperability (JS interop)

Note

This isn't the latest version of this article. For the current release, see the ASP.NET Core 8.0 version of this article.

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

Further JS interop guidance is provided in the following articles:

Note

JavaScript [JSImport]/[JSExport] interop API is available for client-side components in ASP.NET Core in .NET 7 or later.

For more information, see JavaScript JSImport/JSExport interop with ASP.NET Core Blazor.

Throughout this article, the terms server/server-side and client/client-side are used to distinguish locations where app code executes:

  • Server/server-side: Interactive server-side rendering (interactive SSR) of a Blazor Web App.
  • Client/client-side
    • Client-side rendering (CSR) of a Blazor Web App.
    • A Blazor WebAssembly app.

Documentation component examples usually don't configure an interactive render mode with an @rendermode directive in the component's definition file (.razor):

  • In a Blazor Web App, the component must have an interactive render mode applied, either in the component's definition file or inherited from a parent component. For more information, see ASP.NET Core Blazor render modes.

  • In a standalone Blazor WebAssembly app, the components function as shown and don't require a render mode because components always run interactively on WebAssembly in a Blazor WebAssembly app.

When using the Interactive WebAssembly or Interactive Auto render modes, component code sent to the client can be decompiled and inspected. Don't place private code, app secrets, or other sensitive information in client-rendered components.

  • Server/server-side
    • The Server project of a hosted Blazor WebAssembly app.
    • A Blazor Server app.
  • Client/client-side
    • The Client project of a hosted Blazor WebAssembly app.
    • A Blazor WebAssembly app.

For guidance on the purpose and locations of files and folders, see ASP.NET Core Blazor project structure, which also describes the location of the Blazor start script and the location of <head> and <body> content.

The best way to run the demonstration code is to download the BlazorSample_{PROJECT TYPE} sample apps from the Blazor samples GitHub repository that matches the version of .NET that you're targeting. Not all of the documentation examples are currently in the sample apps, but an effort is currently underway to move most of the .NET 8 article examples into the .NET 8 sample apps. This work will be completed in the first quarter of 2024.

JavaScript interop abstractions and features package

The @microsoft/dotnet-js-interop package (npmjs.com) (Microsoft.JSInterop NuGet package) provides abstractions and features for interop between .NET and JavaScript (JS) code. Reference source is available in the dotnet/aspnetcore GitHub repository (/src/JSInterop folder). For more information, see the GitHub repository's README.md file.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the Switch branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205).

Additional resources for writing JS interop scripts in TypeScript:

Interaction with the DOM

Only mutate the DOM with JavaScript (JS) when the object doesn't interact with Blazor. Blazor maintains representations of the DOM and interacts directly with DOM objects. If an element rendered by Blazor is modified externally using JS directly or via JS Interop, the DOM may no longer match Blazor's internal representation, which can result in undefined behavior. Undefined behavior may merely interfere with the presentation of elements or their functions but may also introduce security risks to the app or server.

This guidance not only applies to your own JS interop code but also to any JS libraries that the app uses, including anything provided by a third-party framework, such as Bootstrap JS and jQuery.

In a few documentation examples, JS interop is used to mutate an element purely for demonstration purposes as part of an example. In those cases, a warning appears in the text.

For more information, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor.

Asynchronous JavaScript calls

JS interop calls are asynchronous by default, regardless of whether the called code is synchronous or asynchronous. Calls are asynchronous by default to ensure that components are compatible across server-side and client-side rendering models. When adopting server-side rendering, JS interop calls must be asynchronous because they're sent over a network connection. For apps that exclusively adopt client-side rendering, synchronous JS interop calls are supported.

Object serialization

Blazor uses System.Text.Json for serialization with the following requirements and default behaviors:

  • Types must have a default constructor, get/set accessors must be public, and fields are never serialized.
  • Global default serialization isn't customizable to avoid breaking existing component libraries, impacts on performance and security, and reductions in reliability.
  • Serializing .NET member names results in lowercase JSON key names.
  • JSON is deserialized as JsonElement C# instances, which permit mixed casing. Internal casting for assignment to C# model properties works as expected in spite of any case differences between JSON key names and C# property names.
  • Complex framework types, such as KeyValuePair, might be trimmed away by the IL Trimmer on publish and not present for JS interop. We recommend creating custom types for types that the IL Trimmer trims away by default.

JsonConverter API is available for custom serialization. Properties can be annotated with a [JsonConverter] attribute to override default serialization for an existing data type.

For more information, see the following resources in the .NET documentation:

Blazor supports optimized byte array JS interop that avoids encoding/decoding byte arrays into Base64. The app can apply custom serialization and pass the resulting bytes. For more information, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor.

Blazor supports unmarshalled JS interop when a high volume of .NET objects are rapidly serialized or when large .NET objects or many .NET objects must be serialized. For more information, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor.

DOM cleanup tasks during component disposal

Don't execute JS interop code for DOM cleanup tasks during component disposal. Instead, use the MutationObserver pattern in JavaScript (JS) on the client for the following reasons:

  • The component may have been removed from the DOM by the time your cleanup code executes in Dispose{Async}.
  • During server-side rendering, the Blazor renderer may have been disposed by the framework by the time your cleanup code executes in Dispose{Async}.

The MutationObserver pattern allows you to run a function when an element is removed from the DOM.

In the following example, the DOMCleanup component:

  • Contains a <div> with an id of cleanupDiv. The <div> element is removed from the DOM along with the rest of the component's DOM markup when the component is removed from the DOM.
  • Loads the DOMCleanup JS class from the DOMCleanup.razor.js file and calls its createObserver function to set up the MutationObserver callback. These tasks are accomplished in the OnAfterRenderAsync lifecycle method.

DOMCleanup.razor:

@page "/dom-cleanup"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>DOM Cleanup Example</h1>

<div id="cleanupDiv"></div>

@code {
    private IJSObjectReference? module;

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

            await module.InvokeVoidAsync("DOMCleanup.createObserver");
        }
    }

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

In the following example, the MutationObserver callback is executed each time a DOM change occurs. Execute your cleanup code when the if statement confirms that the target element (cleanupDiv) was removed (if (targetRemoved) { ... }). It's important to disconnect and delete the MutationObserver to avoid a memory leak after your cleanup code executes.

DOMCleanup.razor.js placed side-by-side with the preceding DOMCleanup component:

export class DOMCleanup {
  static observer;

  static createObserver() {
    const target = document.querySelector('#cleanupDiv');

    this.observer = new MutationObserver(function (mutations) {
      const targetRemoved = mutations.some(function (mutation) {
        const nodes = Array.from(mutation.removedNodes);
        return nodes.indexOf(target) !== -1;
      });

      if (targetRemoved) {
        // Cleanup resources here
        // ...

        // Disconnect and delete MutationObserver
        this.observer && this.observer.disconnect();
        delete this.observer;
      }
    });

    this.observer.observe(target.parentNode, { childList: true });
  }
}

window.DOMCleanup = DOMCleanup;

JavaScript interop calls without a circuit

This section only applies to server-side apps.

JavaScript (JS) interop calls can't be issued after a SignalR circuit is disconnected. Without a circuit during component disposal or at any other time that a circuit doesn't exist, the following method calls fail and log a message that the circuit is disconnected as a JSDisconnectedException:

In order to avoid logging JSDisconnectedException or to log custom information, catch the exception in a try-catch statement.

For the following component disposal example:

  • The component implements IAsyncDisposable.
  • objInstance is an IJSObjectReference.
  • JSDisconnectedException is caught and not logged.
  • Optionally, you can log custom information in the catch statement at whatever log level you prefer. The following example doesn't log custom information because it assumes the developer doesn't care about when or where circuits are disconnected during component disposal.
async ValueTask IAsyncDisposable.DisposeAsync()
{
    try
    {
        if (objInstance is not null)
        {
            await objInstance.DisposeAsync();
        }
    }
    catch (JSDisconnectedException)
    {
    }
}

If you must clean up your own JS objects or execute other JS code on the client after a circuit is lost, use the MutationObserver pattern in JS on the client. The MutationObserver pattern allows you to run a function when an element is removed from the DOM.

For more information, see the following articles:

JavaScript location

Load JavaScript (JS) code using any of the following approaches:

Warning

Only place a <script> tag in a component file (.razor) if the component is guaranteed to adopt static server-side rendering (static SSR) because the <script> tag can't be updated dynamically.

Warning

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

Note

Documentation examples usually place scripts in a <script> tag or load global scripts from external files. These approaches pollute the client with global functions. For production apps, we recommend placing JavaScript into separate JavaScript modules that can be imported when needed. For more information, see the JavaScript isolation in JavaScript modules section.

Note

Documentation examples place scripts into a <script> tag or load global scripts from external files. These approaches pollute the client with global functions. Placing JavaScript into separate JavaScript modules that can be imported when needed is not supported in Blazor earlier than ASP.NET Core 5.0. If the app requires the use of JS modules for JS isolation, we recommend using ASP.NET Core 5.0 or later to build the app. For more information, use the Version dropdown list to select a 5.0 or later version of this article and see the JavaScript isolation in JavaScript modules section.

Load a script in <head> markup

The approach in this section isn't generally recommended.

Place the JavaScript (JS) tags (<script>...</script>) in the <head> element markup:

<head>
    ...

    <script>
      window.jsMethod = (methodParameter) => {
        ...
      };
    </script>
</head>

Loading JS from the <head> isn't the best approach for the following reasons:

  • JS interop may fail if the script depends on Blazor. We recommend loading scripts using one of the other approaches, not via the <head> markup.
  • The page may become interactive slower due to the time it takes to parse the JS in the script.

Load a script in <body> markup

Place the JavaScript (JS) tags (<script>...</script>) inside the closing </body> element after the Blazor script reference:

<body>
    ...

    <script src="{BLAZOR SCRIPT}"></script>
    <script>
      window.jsMethod = (methodParameter) => {
        ...
      };
    </script>
</body>

In the preceding example, the {BLAZOR SCRIPT} placeholder is the Blazor script path and file name. For the location of the script, see ASP.NET Core Blazor project structure.

Load a script from an external JavaScript file (.js) collocated with a component

Collocation of JavaScript (JS) files for Razor components is a convenient way to organize scripts in an app.

Razor components of Blazor apps collocate JS files using the .razor.js extension and are publicly addressable using the path to the file in the project:

{PATH}/{COMPONENT}.{EXTENSION}.js

  • The {PATH} placeholder is the path to the component.
  • The {COMPONENT} placeholder is the component.
  • The {EXTENSION} placeholder matches the extension of the component (razor).

When the app is published, the framework automatically moves the script to the web root. Scripts are moved to bin/Release/{TARGET FRAMEWORK MONIKER}/publish/wwwroot/{PATH}/Pages/{COMPONENT}.razor.js, where the placeholders are:

No change is required to the script's relative URL, as Blazor takes care of placing the JS file in published static assets for you.

This section and the following examples are primarily focused on explaining JS file collocation. The first example demonstrates a collocated JS file with an ordinary JS function. The second example demonstrates the use of a module to load a function, which is the recommended approach for most production apps. Calling JS from .NET is fully covered in Call JavaScript functions from .NET methods in ASP.NET Core Blazor, where there are further explanations of the Blazor JS API with additional examples. Component disposal, which is present in the second example, is covered in ASP.NET Core Razor component lifecycle.

The following JsCollocation1 component loads a script via a HeadContent component and calls a JS function with IJSRuntime.InvokeAsync. The {PATH} placeholder is the path to the component.

Important

If you use the following code for a demonstration in a test app, change the {PATH} placeholder to the path of the component (example: Components/Pages in .NET 8 or later or Pages in .NET 7 or earlier). In a Blazor Web App (.NET 8 or later), the component requires an interactive render mode applied either globally to the app or to the component definition.

Add the following script after the Blazor script (location of the Blazor start script):

<script src="{PATH}/JsCollocation1.razor.js"></script>

JsCollocation1 component ({PATH}/JsCollocation1.razor):

@page "/js-collocation-1"
@inject IJSRuntime JS

<PageTitle>JS Collocation 1</PageTitle>

<h1>JS Collocation Example 1</h1>

<button @onclick="ShowPrompt">Call showPrompt1</button>

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

@code {
    private string? result;

    public async void ShowPrompt()
    {
        result = await JS.InvokeAsync<string>(
            "showPrompt1", "What's your name?");
        StateHasChanged();
    }
}

The collocated JS file is placed next to the JsCollocation1 component file with the file name JsCollocation1.razor.js. In the JsCollocation1 component, the script is referenced at the path of the collocated file. In the following example, the showPrompt1 function accepts the user's name from a Window prompt() and returns it to the JsCollocation1 component for display.

{PATH}/JsCollocation1.razor.js:

function showPrompt1(message) {
  return prompt(message, 'Type your name here');
}

The preceding approach isn't recommended for general use in production apps because it pollutes the client with global functions. A better approach for production apps is to use JS modules. The same general principles apply to loading a JS module from a collocated JS file, as the next example demonstrates.

The following JsCollocation2 component's OnAfterRenderAsync method loads a JS module into module, which is an IJSObjectReference of the component class. module is used to call the showPrompt2 function. The {PATH} placeholder is the path to the component.

Important

If you use the following code for a demonstration in a test app, change the {PATH} placeholder to the path of the component. In a Blazor Web App (.NET 8 or later), the component requires an interactive render mode applied either globally to the app or to the component definition.

JsCollocation2 component ({PATH}/JsCollocation2.razor):

@page "/js-collocation-2"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>JS Collocation 2</PageTitle>

<h1>JS Collocation Example 2</h1>

<button @onclick="ShowPrompt">Call showPrompt2</button>

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

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

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            /*
                Change the {PATH} placeholder in the next line to the path of
                the collocated JS file in the app. Examples:

                ./Components/Pages/JsCollocation2.razor.js (.NET 8 or later)
                ./Pages/JsCollocation2.razor.js (.NET 7 or earlier)
            */
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./{PATH}/JsCollocation2.razor.js");
        }
    }

    public async void ShowPrompt()
    {
        if (module is not null)
        {
            result = await module.InvokeAsync<string>(
                "showPrompt2", "What's your name?");
            StateHasChanged();
        }
    }

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

{PATH}/JsCollocation2.razor.js:

export function showPrompt2(message) {
  return prompt(message, 'Type your name here');
}

For scripts or modules provided by a Razor class library (RCL), the following path is used:

_content/{PACKAGE ID}/{PATH}/{COMPONENT}.{EXTENSION}.js

  • The {PACKAGE ID} placeholder is the RCL's package identifier (or library name for a class library referenced by the app).
  • The {PATH} placeholder is the path to the component. If a Razor component is located at the root of the RCL, the path segment isn't included.
  • The {COMPONENT} placeholder is the component name.
  • The {EXTENSION} placeholder matches the extension of component, either razor or cshtml.

In the following Blazor app example:

  • The RCL's package identifier is AppJS.
  • A module's scripts are loaded for the JsCollocation3 component (JsCollocation3.razor).
  • The JsCollocation3 component is in the Components/Pages folder of the RCL.
module = await JS.InvokeAsync<IJSObjectReference>("import", 
    "./_content/AppJS/Components/Pages/JsCollocation3.razor.js");

For more information on RCLs, see Consume ASP.NET Core Razor components from a Razor class library (RCL).

Load a script from an external JavaScript file (.js)

Place the JavaScript (JS) tags (<script>...</script>) with a script source (src) path inside the closing </body> element after the Blazor script reference:

<body>
    ...

    <script src="{BLAZOR SCRIPT}"></script>
    <script src="{SCRIPT PATH AND FILE NAME (.js)}"></script>
</body>

In the preceding example:

  • The {BLAZOR SCRIPT} placeholder is the Blazor script path and file name. For the location of the script, see ASP.NET Core Blazor project structure.
  • The {SCRIPT PATH AND FILE NAME (.js)} placeholder is the path and script file name under wwwroot.

In the following example of the preceding <script> tag, the scripts.js file is in the wwwroot/js folder of the app:

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

You can also serve scripts directly from the wwwroot folder if you prefer not to keep all of your scripts in a separate folder under wwwroot:

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

When the external JS file is supplied by a Razor class library, specify the JS file using its stable static web asset path: ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILE NAME (.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.
  • The {SCRIPT PATH AND FILE NAME (.js)} placeholder is the path and file name under wwwroot.
<body>
    ...

    <script src="{BLAZOR SCRIPT}"></script>
    <script src="./_content/{PACKAGE ID}/{SCRIPT PATH AND FILE NAME (.js)}"></script>
</body>

In the following example of the preceding <script> tag:

  • The Razor class library has an assembly name of ComponentLibrary, and a <PackageId> isn't specified in the library's project file.
  • The scripts.js file is in the class library's wwwroot folder.
<script src="./_content/ComponentLibrary/scripts.js"></script>

For more information, see Consume ASP.NET Core Razor components from a Razor class library (RCL).

Inject a script before or after Blazor starts

To ensure scripts load before or after Blazor starts, use a JavaScript initializer. For more information and examples, see ASP.NET Core Blazor startup.

Inject a script after Blazor starts

To inject a script after Blazor starts, chain to the Promise that results from a manual start of Blazor. For more information and an example, see ASP.NET Core Blazor startup.

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 more information, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor.

Dynamic import with the import() operator is supported with ASP.NET Core and Blazor:

if ({CONDITION}) import("/additionalModule.js");

In the preceding example, the {CONDITION} placeholder represents a conditional check to determine if the module should be loaded.

For browser compatibility, see Can I use: JavaScript modules: dynamic import.

Cached JavaScript files

JavaScript (JS) files and other static assets aren't generally cached on clients during development in the Development environment. During development, static asset requests include the Cache-Control header with a value of no-cache or max-age with a value of zero (0).

During production in the Production environment, JS files are usually cached by clients.

To disable client-side caching in browsers, developers usually adopt one of the following approaches:

For more information, see:

Size limits on JavaScript interop calls

This section only applies to interactive components in server-side apps. For client-side components, the framework doesn't impose a limit on the size of JavaScript (JS) interop inputs and outputs.

For interactive components in server-side apps, JS interop calls passing data from the client to the server 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. For more information on the size limit, error messages, and guidance on dealing with message size limits, see ASP.NET Core Blazor SignalR guidance.

Determine where the app is running

If it's relevant for the app to know where code is running for JS interop calls, use OperatingSystem.IsBrowser to determine if the component is executing in the context of browser on WebAssembly.