Tutorial de Tauri – Capítulo 8: Eventos – Comunicación bidireccional

Los commands funcionan bien para peticiones puntuales: JavaScript pide, Rust responde. Pero hay situaciones donde necesitas comunicación continua o en tiempo real.

¿Qué pasa si Rust necesita notificar al frontend de algo? ¿O si quieres mostrar el progreso de una operación larga? Para eso existen los eventos.

Diferencia entre Commands y Events

MecanismoDirecciónUso típico
CommandsJS → RustPeticiones puntuales
EventsBidireccionalNotificaciones, progreso, tiempo real

Piensa en los commands como una llamada telefónica (llamas, te responden) y en los eventos como una radio (emites, y quien esté escuchando recibe el mensaje).

Preparar el proyecto

Vamos a añadir eventos a la misma aplicación que usamos en el capítulo anterior. Si tienes npm run tauri dev ejecutándose, déjalo abierto — los cambios se recompilarán automáticamente.

Primero necesitamos añadir tokio a las dependencias, porque vamos a simular un proceso que tarda. Abre src-tauri/Cargo.toml y añade esta línea dentro de la sección [dependencies]:

tokio = { version = "1", features = ["time"] }

Emitir eventos desde Rust al frontend

Imagina un proceso que tarda 5 segundos y quieres mostrar el progreso al usuario. Con un command normal no podrías, porque JavaScript se queda esperando hasta que Rust termine. Con eventos, Rust puede ir enviando actualizaciones mientras trabaja.

Abre src-tauri/src/lib.rs y añade estos imports al principio del archivo, junto a los que ya tienes:

use tauri::{AppHandle, Emitter};

Ahora añade el struct ProgresoPayload y el command proceso_largo. Ponlos junto a tus otros structs y commands:

#[derive(Clone, Serialize)]
struct ProgresoPayload {
    porcentaje: u32,
    mensaje: String,
}

#[tauri::command]
async fn proceso_largo(app: AppHandle) -> Result<String, String> {
    for i in 1..=10 {
        // Pausa medio segundo (simula trabajo real)
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;

        // Envía un evento de progreso al frontend
        app.emit("progreso", ProgresoPayload {
            porcentaje: i * 10,
            mensaje: format!("Procesando paso {} de 10", i),
        }).map_err(|e| e.to_string())?;
    }

    Ok("Proceso completado".to_string())
}

Veamos qué hace cada parte:

  • #[derive(Clone, Serialize)]: el struct necesita Serialize para convertirse a JSON (como vimos en el capítulo anterior) y Clone porque Tauri necesita poder copiar el dato al enviarlo como evento.
  • ProgresoPayload: define los datos que enviamos con cada evento, un porcentaje y un mensaje de texto.
  • async fn proceso_largo(app: AppHandle): es un command asíncrono. El parámetro app lo inyecta Tauri automáticamente (no lo pasas desde JavaScript). Nos da acceso para emitir eventos.
  • for i in 1..=10: un bucle que va del 1 al 10 (incluido). Simula 10 pasos de trabajo.
  • tokio::time::sleep(...).await: pausa medio segundo entre cada paso, simulando que el proceso tarda.
  • app.emit("progreso", ...): esta es la línea clave. Envía un evento llamado "progreso" con los datos del payload al frontend. Cualquier componente que esté escuchando este evento lo recibirá.

Ahora registra el command proceso_largo en la función run() de src-tauri/src/lib.rs:

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![saludar, calcular, procesar_usuario, obtener_usuario, proceso_largo])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Escuchar eventos en JavaScript

Ahora necesitamos un componente que escuche los eventos de progreso y muestre una barra que avanza en tiempo real. Crea el archivo src/ProcesoLargo.jsx:

import { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";

function ProcesoLargo() {
  const [progreso, setProgreso] = useState(0);
  const [mensaje, setMensaje] = useState("");
  const [ejecutando, setEjecutando] = useState(false);

  useEffect(() => {
    // Suscribirse al evento "progreso" que emite Rust
    const unlisten = listen("progreso", (event) => {
      setProgreso(event.payload.porcentaje);
      setMensaje(event.payload.mensaje);
    });

    // Al desmontar el componente, cancelar la suscripción
    return () => {
      unlisten.then((fn) => fn());
    };
  }, []);

  async function iniciar() {
    setEjecutando(true);
    setProgreso(0);

    try {
      const resultado = await invoke("proceso_largo");
      setMensaje(resultado);
    } catch (error) {
      setMensaje("Error: " + error);
    } finally {
      setEjecutando(false);
    }
  }

  return (
    <div>
      <h2>Proceso largo</h2>
      <button onClick={iniciar} disabled={ejecutando}>
        {ejecutando ? "Procesando..." : "Iniciar proceso"}
      </button>

      <div style={{ background: "#333", borderRadius: "8px", overflow: "hidden", marginTop: "1rem" }}>
        <div
          style={{
            width: `${progreso}%`,
            background: "#24c8db",
            padding: "8px 0",
            transition: "width 0.3s",
          }}
        />
      </div>

      <p>{progreso}% - {mensaje}</p>
    </div>
  );
}

export default ProcesoLargo;

Veamos las partes clave de este componente:

  • import { listen } from "@tauri-apps/api/event": importamos la función listen, que nos permite suscribirnos a eventos de Tauri.
  • listen("progreso", callback): se suscribe al evento "progreso". Cada vez que Rust llama a app.emit("progreso", ...), se ejecuta el callback con los datos.
  • event.payload: contiene los datos que Rust envió (el porcentaje y el mensaje del struct ProgresoPayload).
  • useEffect: suscribe el listener cuando el componente aparece en pantalla, y lo cancela cuando desaparece. El return dentro del useEffect es la función de limpieza — evita que queden listeners activos si el componente se desmonta.
  • invoke("proceso_largo"): lanza el command de Rust. Mientras Rust trabaja en su bucle, los eventos van llegando uno a uno y actualizando la barra de progreso.
  • disabled={ejecutando}: desactiva el botón mientras el proceso está en marcha, para que el usuario no lo pulse dos veces.

Añade el componente <ProcesoLargo /> en src/App.jsx:

import Greeting from "./Greeting";
import Calculator from "./Calculator";
import UserCard from "./UserCard";
import ProcesoLargo from "./ProcesoLargo";

function App() {
  return (
    <div className="container">
      <h1>Mi App Tauri + React</h1>
      <Greeting />
      <Calculator />
      <UserCard />
      <ProcesoLargo />
    </div>
  );
}

export default App;

Ejecuta npm run tauri dev (o espera a que se recompile si ya lo tenías abierto), baja hasta el componente «Proceso largo» y pulsa «Iniciar proceso». Verás cómo la barra de progreso avanza del 0% al 100% en tiempo real, actualizándose cada medio segundo con los eventos que Rust envía.

Emitir eventos desde JavaScript

Los eventos también pueden ir en la dirección contraria: del frontend a Rust. Esto es menos habitual (para enviar datos a Rust normalmente usarás commands), pero es útil cuando quieres notificar al backend de algo sin esperar respuesta.

Desde JavaScript, emites un evento con la función emit:

import { emit } from "@tauri-apps/api/event";

// Enviar un evento a Rust
await emit("mi-evento", { dato: "valor" });

Y en Rust, lo escuchas dentro del setup de la aplicación en src-tauri/src/lib.rs:

use tauri::Listener;

pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            app.listen("mi-evento", |event| {
                println!("Evento recibido: {:?}", event.payload());
            });
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![...])
        .run(tauri::generate_context!())
        .expect("error");
}

No necesitas añadir esto a tu proyecto ahora. Es una referencia para cuando lo necesites. En la práctica, la dirección más común es Rust → JavaScript (como la barra de progreso que acabamos de construir).

Escuchar un evento solo una vez

Si solo necesitas recibir un evento una vez (por ejemplo, una notificación que solo ocurre al inicio), usa once en lugar de listen:

import { once } from "@tauri-apps/api/event";

await once("evento-unico", (event) => {
  console.log("Recibido:", event.payload);
  // Este callback solo se ejecutará una vez
});

Cuándo usar cada uno

SituaciónUsa
Pedir datos al backendCommand
Enviar un formularioCommand
Mostrar progreso de una operaciónEvent
Actualizaciones en tiempo realEvent
Notificaciones del sistemaEvent

Resumen

Los eventos complementan a los commands:

  • Emitir desde Rust con app.emit() (necesita use tauri::Emitter)
  • Escuchar en JavaScript con listen()
  • Limpiar suscripciones correctamente en el useEffect
  • Usar once() para eventos que solo ocurren una vez
  • Commands para peticiones puntuales, eventos para comunicación continua

En el próximo capítulo aprenderás a acceder al sistema de archivos desde Tauri, combinando todo lo que has aprendido hasta ahora.

Nos vemos en el Capítulo 9.


¿Te está gustando este tutorial?

Este tutorial forma parte del libro Tauri 2.0: Aplicaciones de Escritorio con React y Rust, disponible en Amazon en formato papel y ebook. El libro incluye todos los capítulos, tres proyectos completos paso a paso y contenido exclusivo que no encontrarás en el blog.

Un saludo, y si aún no lo has hecho no olvides suscribirte a mi blog para no perderte los próximos posts  :-),

También puedes seguirme en Twitter en ‎@revi_apps y no olvides que me ayudas mucho si compartes este post en las redes sociales.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Scroll al inicio