C.W.K.
Stream
Lesson 04 of 06 · published

Events: emit & listen

~13 min · tauri, events, ipc, frontend

Level 0Web Tourist
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Commands are a question and an answer. Events are an announcement nobody had to ask for."

Push, Not Pull

Sometimes the core has news the frontend didn't request: a download finished, a watched file changed, a timer ticked, a tray menu was clicked. Polling for that with commands is wasteful. Instead the core emits an event and the frontend listens. app.emit("name", payload) broadcasts to all webviews; the frontend's listen("name", handler) (from @tauri-apps/api/event) receives each one with its payload. The payload is any serializable value, serde-encoded just like command data.

Broadcast vs Target

emit sends to everyone. When you have multiple windows and only one should hear it, emit_to("window-label", "name", payload) targets a specific webview. Events also flow the other direction — the frontend can emit and Rust can listen — but for frontend-to-core requests you usually want a command (you get a return value and error handling). Reserve events for fire-and-forget announcements.

Always Clean Up Listeners

listen returns a function that removes the listener. In a component, register in an effect and call that unlisten function on cleanup, or you'll stack duplicate handlers every time the component mounts — the classic memory-leak-and-double-fire bug. Use once for events you only ever want to handle a single time.

Code

Emitting from the core·rust
use tauri::{AppHandle, Manager, Emitter};
use serde::Serialize;

#[derive(Clone, Serialize)]
struct Progress { done: u32, total: u32 }

fn report(app: &AppHandle) {
    // Broadcast to every webview listening for "progress".
    let _ = app.emit("progress", Progress { done: 7, total: 10 });
    // Or target one window by its label:
    let _ = app.emit_to("main", "progress", Progress { done: 7, total: 10 });
}
Listening on the frontend (with cleanup)·tsx
import { listen } from "@tauri-apps/api/event";
import { useEffect, useState } from "react";

function ProgressBar() {
  const [p, setP] = useState({ done: 0, total: 0 });
  useEffect(() => {
    // listen resolves to an unlisten function — call it on cleanup.
    const stop = listen<{ done: number; total: number }>("progress", (e) =>
      setP(e.payload),
    );
    return () => { stop.then((un) => un()); };
  }, []);
  return <progress value={p.done} max={p.total} />;
}

External links

Exercise

Make the background thread from the AppHandle lesson emit a 'tick' event every second with an incrementing number. On the frontend, listen for it and render the latest value — with proper cleanup. Then intentionally remove the cleanup and watch what happens if the component remounts (open React devtools or toggle it): the doubled handlers are the lesson.
Hint
Rust: handle.emit("tick", n) in the loop. React: const stop = listen('tick', ...); return () => stop.then(u => u()). To see the leak, comment out the return cleanup and remount the component.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.