La interoperabilidad con JavaScript es el mecanismo con el que el código .NET y el entorno JavaScript intercambian llamadas y datos.
Blazor permite escribir gran parte de la interfaz con C#, pero seguimos necesitando JavaScript para algunas API del navegador y para integrar bibliotecas como Chart.js.
Blazor no aísla tu aplicación en una burbuja; al contrario, te permite interactuar con el ecosistema web mediante el mecanismo conocido como JS Interop.
El servicio IJSRuntime
La puerta de entrada a JavaScript es una abstracción: la interfaz IJSRuntime.
Como cualquier otro servicio en Blazor, debemos inyectarlo en nuestro componente.
@inject IJSRuntime JSEste servicio se encarga de serializar tus datos en JSON, enviarlos al entorno de JavaScript (navegador), ejecutar la función que pidas y devolverte el resultado deserializado.
Definiendo la función JavaScript
En este primer ejemplo, la función será accesible globalmente desde window. Más adelante la moveremos a un módulo para no contaminar el ámbito global.
Para una demostración puedes cargar este script desde el documento raíz. En aplicaciones reales se recomiendan archivos externos y módulos JS, no etiquetas <script> dentro de componentes interactivos.
Vamos a añadir un script simple en nuestro HTML base:
<script>
window.funcionesUtiles = {
mostrarAlerta: function (mensaje) {
alert(mensaje);
},
pedirConfirmacion: function (mensaje) {
return confirm(mensaje); // Devuelve true o false
},
sumarNumeros: function (a, b) {
return a + b;
}
};
</script>Llamando desde C#
El servicio IJSRuntime nos ofrece dos métodos principales, ambos asíncronos.
InvokeVoidAsync para llamadas sin resultado
Usamos este método cuando queremos ejecutar una acción en JS y no esperamos ningún valor de retorno (o no nos importa). Es ideal para efectos secundarios como logs, alertas o manipulación del DOM.
<button @onclick="LanzarAlerta">Saludar</button>
@code {
private async Task LanzarAlerta()
{
// 1er parámetro: Nombre de la función JS
// 2do parámetro: Argumento(s) para la función
await JS.InvokeVoidAsync("funcionesUtiles.mostrarAlerta", "¡Hola desde C#!");
}
}Aunque la función JS no devuelva nada, en Blazor Server es importante usar await. La llamada viaja por la red (SignalR) hasta el navegador, se ejecuta y vuelve la confirmación. Si no esperas (await), podrías tener condiciones de carrera.
InvokeAsync<T> para obtener un resultado
Usamos este método cuando la función JS devuelve un valor que necesitamos en C#. Blazor deserializará automáticamente el resultado al tipo T que especifiquemos.
<button @onclick="ConfirmarBorrado">Borrar Archivo</button>
<p>Resultado: @mensaje</p>
@code {
private string mensaje = string.Empty;
private async Task ConfirmarBorrado()
{
// Esperamos un booleano de vuelta
bool confirmado = await JS.InvokeAsync<bool>(
"funcionesUtiles.pedirConfirmacion",
"¿Seguro que quieres borrar esto?");
if (confirmado)
{
mensaje = "Archivo borrado ✅";
// Lógica de borrado...
}
else
{
mensaje = "Operación cancelada ❌";
}
}
}Blazor maneja tipos primitivos (int, string, bool) y objetos complejos (DTOs) sin problemas, siempre que sean serializables a JSON.
El momento adecuado para acceder al DOM
Durante el prerenderizado no se puede llamar a JavaScript. Además, una referencia a un elemento todavía no existe en OnInitialized, porque el navegador aún no ha renderizado ese elemento.
Para inicializar una biblioteca sobre el DOM, espera a OnAfterRenderAsync. Las llamadas que no dependen del DOM también pueden hacerse desde un manejador de eventos interactivo.
@inject IJSRuntime JS
<input @ref="miInput" placeholder="Foco automático..." />
@code {
private ElementReference miInput;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Solo queremos poner el foco la primera vez que se carga la página
if (firstRender)
{
await JS.InvokeVoidAsync("funcionesUtiles.enfocar", miInput);
}
}
}La función JavaScript recibe la referencia al elemento:
window.funcionesUtiles.enfocar = element => element.focus();Encapsular las llamadas
Llenar tus componentes de llamadas JS.InvokeAsync es una mala práctica. Ensucia el código y lo hace difícil de testear.
La recomendación “Luis Llamas” es encapsular estas llamadas en un servicio C#.
Mala práctica:
// En el componente
await JS.InvokeVoidAsync("localStorage.setItem", "clave", "valor");Buena práctica:
Crear un servicio 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);
}
}Así, tu componente solo inyecta ILocalStorageService y no sabe nada de JavaScript. Además, si mañana cambias LocalStorage por IndexedDB, el componente ni se entera.