Tauri 2.0 Events Sending and Receiving Messages

Tauri's event system provides powerful bidirectional communication enabling Rust backend to push updates to the frontend, windows to communicate with each other, and frontend to broadcast messages creating real-time, event-driven desktop applications responding instantly to data changes, user actions, and system events—complementing the invoke API with push-based messaging perfect for notifications, progress updates, live data streaming, and inter-window coordination. Unlike invoke which requires frontend to explicitly request data, events allow backend to proactively notify frontend of changes enabling reactive applications where UI updates automatically when backend state changes without polling, multiple windows stay synchronized through event broadcasting, long-running operations report progress incrementally, and system-level changes trigger immediate UI updates maintaining consistency. This comprehensive guide covers understanding event architecture and message flow patterns, emitting events from Rust backend to specific windows or all windows, listening to events in JavaScript with proper cleanup, using built-in system events for window lifecycle and app state, creating custom event types with structured data payloads, implementing event-driven patterns including pub/sub and message queues, handling event errors and dealing with listener failures, optimizing event performance avoiding memory leaks and excessive emissions, and building real-time features including progress tracking, notifications, and live data updates. Mastering event patterns enables creating sophisticated desktop applications with responsive UIs updating automatically, multiple windows coordinating seamlessly, background tasks reporting progress without blocking, and system integration providing native experience. Before proceeding, understand IPC fundamentals and command creation.
Understanding Event Architecture
Tauri's event system uses a publish-subscribe pattern where emitters send messages to named event channels and listeners subscribe to receive messages enabling decoupled communication between components maintaining clean architecture without tight coupling.
| Event Direction | Method | Use Case | Scope |
|---|---|---|---|
| Rust → Frontend | window.emit() | Progress updates, notifications | Single window |
| Rust → All Windows | app.emit_all() | Global state changes | All windows |
| Frontend → Frontend | emit() | Component communication | Current window |
| Frontend → Rust | emit() + listen in Rust | User actions | Backend handler |
| Window → Window | emit_to() | Inter-window messages | Specific window |
| System Events | Built-in events | Window lifecycle, focus | Automatic |
Emitting Events from Rust
Rust code can emit events to notify the frontend of changes, progress, or errors using window and app handles providing real-time updates without frontend polling maintaining responsive user interfaces.
// src-tauri/src/main.rs
use tauri::{Manager, Window, AppHandle};
use serde::Serialize;
// Simple string event
#[tauri::command]
async fn send_notification(window: Window) {
window.emit("notification", "Hello from Rust!").unwrap();
}
// Event with structured data
#[derive(Clone, Serialize)]
struct ProgressUpdate {
current: u32,
total: u32,
message: String,
}
#[tauri::command]
async fn process_with_progress(window: Window) {
for i in 0..=100 {
// Simulate work
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// Emit progress event
window.emit("progress", ProgressUpdate {
current: i,
total: 100,
message: format!("Processing step {}/100", i),
}).unwrap();
}
// Emit completion event
window.emit("complete", "Processing finished!").unwrap();
}
// Emit to all windows
#[tauri::command]
async fn broadcast_update(app: AppHandle, message: String) {
app.emit_all("global-update", message).unwrap();
}
// Emit to specific window
#[tauri::command]
async fn send_to_window(app: AppHandle, window_label: String, message: String) {
if let Some(target_window) = app.get_window(&window_label) {
target_window.emit("message", message).unwrap();
}
}
// Event with error handling
#[derive(Clone, Serialize)]
struct TaskResult {
success: bool,
data: Option<String>,
error: Option<String>,
}
#[tauri::command]
async fn risky_task(window: Window) {
window.emit("task-started", ()).unwrap();
// Simulate risky operation
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
let result = TaskResult {
success: true,
data: Some("Task completed successfully".to_string()),
error: None,
};
window.emit("task-result", result).unwrap();
}
// Long-running task with multiple event types
#[derive(Clone, Serialize)]
struct DownloadProgress {
bytes_downloaded: u64,
total_bytes: u64,
speed: String,
}
#[tauri::command]
async fn download_file(window: Window, url: String) -> Result<String, String> {
window.emit("download-started", &url).unwrap();
// Simulate download with progress
let total_bytes = 10_000_000u64;
for chunk in 0..10 {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let bytes_downloaded = (chunk + 1) * (total_bytes / 10);
window.emit("download-progress", DownloadProgress {
bytes_downloaded,
total_bytes,
speed: format!("{} MB/s", 2.5),
}).unwrap();
}
window.emit("download-complete", &url).unwrap();
Ok(format!("Downloaded: {}", url))
}
// Background task emitting periodic updates
use std::sync::Arc;
use tokio::sync::Mutex;
#[tauri::command]
async fn start_monitoring(app: AppHandle) {
tokio::spawn(async move {
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
// Get system stats
let stats = get_system_stats();
// Emit to all windows
app.emit_all("system-stats", stats).ok();
}
});
}
#[derive(Clone, Serialize)]
struct SystemStats {
cpu_usage: f32,
memory_usage: f32,
disk_usage: f32,
}
fn get_system_stats() -> SystemStats {
// Simulate getting stats
SystemStats {
cpu_usage: 45.5,
memory_usage: 62.3,
disk_usage: 78.1,
}
}Listening to Events in Frontend
Frontend code subscribes to events using the listen API receiving real-time updates from backend with proper cleanup preventing memory leaks maintaining application performance.
// React: Listening to events
import { useEffect, useState } from "react";
import { listen, UnlistenFn } from "@tauri-apps/api/event";
// Simple event listener
function NotificationDisplay() {
const [message, setMessage] = useState<string>("");
useEffect(() => {
let unlisten: UnlistenFn;
// Set up listener
(async () => {
unlisten = await listen<string>("notification", (event) => {
setMessage(event.payload);
console.log("Received:", event.payload);
});
})();
// Cleanup on unmount
return () => {
if (unlisten) unlisten();
};
}, []);
return <div>{message && <p>{message}</p>}</div>;
}
// Progress tracking
interface ProgressUpdate {
current: number;
total: number;
message: string;
}
function ProgressTracker() {
const [progress, setProgress] = useState<ProgressUpdate | null>(null);
const [complete, setComplete] = useState(false);
useEffect(() => {
let unlistenProgress: UnlistenFn;
let unlistenComplete: UnlistenFn;
(async () => {
// Listen for progress updates
unlistenProgress = await listen<ProgressUpdate>(
"progress",
(event) => {
setProgress(event.payload);
}
);
// Listen for completion
unlistenComplete = await listen<string>("complete", (event) => {
setComplete(true);
console.log("Process complete:", event.payload);
});
})();
return () => {
if (unlistenProgress) unlistenProgress();
if (unlistenComplete) unlistenComplete();
};
}, []);
if (complete) {
return <div>✅ Processing complete!</div>;
}
if (!progress) {
return <div>Waiting to start...</div>;
}
const percentage = (progress.current / progress.total) * 100;
return (
<div>
<div>Progress: {percentage.toFixed(1)}%</div>
<progress value={progress.current} max={progress.total} />
<div>{progress.message}</div>
</div>
);
}
// Custom hook for event listening
function useEventListener<T>(
eventName: string,
handler: (payload: T) => void
) {
useEffect(() => {
let unlisten: UnlistenFn;
(async () => {
unlisten = await listen<T>(eventName, (event) => {
handler(event.payload);
});
})();
return () => {
if (unlisten) unlisten();
};
}, [eventName, handler]);
}
// Usage of custom hook
function SystemMonitor() {
const [stats, setStats] = useState<SystemStats | null>(null);
useEventListener<SystemStats>("system-stats", setStats);
if (!stats) return <div>Loading stats...</div>;
return (
<div>
<p>CPU: {stats.cpu_usage}%</p>
<p>Memory: {stats.memory_usage}%</p>
<p>Disk: {stats.disk_usage}%</p>
</div>
);
}
interface SystemStats {
cpu_usage: number;
memory_usage: number;
disk_usage: number;
}
// Download progress with multiple events
interface DownloadProgress {
bytes_downloaded: number;
total_bytes: number;
speed: string;
}
function FileDownloader() {
const [status, setStatus] = useState<string>("idle");
const [progress, setProgress] = useState<DownloadProgress | null>(null);
useEffect(() => {
const listeners: UnlistenFn[] = [];
(async () => {
// Download started
listeners.push(
await listen<string>("download-started", (event) => {
setStatus("downloading");
console.log("Started:", event.payload);
})
);
// Progress updates
listeners.push(
await listen<DownloadProgress>("download-progress", (event) => {
setProgress(event.payload);
})
);
// Download complete
listeners.push(
await listen<string>("download-complete", (event) => {
setStatus("complete");
console.log("Complete:", event.payload);
})
);
})();
return () => {
listeners.forEach((unlisten) => unlisten && unlisten());
};
}, []);
const percentage = progress
? (progress.bytes_downloaded / progress.total_bytes) * 100
: 0;
return (
<div>
<p>Status: {status}</p>
{progress && (
<>
<progress value={percentage} max="100" />
<p>{percentage.toFixed(1)}%</p>
<p>Speed: {progress.speed}</p>
</>
)}
</div>
);
}<!-- Vue 3: Listening to events -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { listen, UnlistenFn } from "@tauri-apps/api/event";
// Simple notification
const message = ref<string>("");
let unlisten: UnlistenFn;
onMounted(async () => {
unlisten = await listen<string>("notification", (event) => {
message.value = event.payload;
});
});
onUnmounted(() => {
if (unlisten) unlisten();
});
// Progress tracking
interface ProgressUpdate {
current: number;
total: number;
message: string;
}
const progress = ref<ProgressUpdate | null>(null);
const complete = ref(false);
let unlistenProgress: UnlistenFn;
let unlistenComplete: UnlistenFn;
onMounted(async () => {
unlistenProgress = await listen<ProgressUpdate>("progress", (event) => {
progress.value = event.payload;
});
unlistenComplete = await listen<string>("complete", (event) => {
complete.value = true;
});
});
onUnmounted(() => {
if (unlistenProgress) unlistenProgress();
if (unlistenComplete) unlistenComplete();
});
// Computed percentage
const percentage = computed(() => {
if (!progress.value) return 0;
return (progress.value.current / progress.value.total) * 100;
});
</script>
<template>
<div>
<p v-if="message">{{ message }}</p>
<div v-if="complete">✅ Processing complete!</div>
<div v-else-if="progress">
<p>Progress: {{ percentage.toFixed(1) }}%</p>
<progress :value="progress.current" :max="progress.total" />
<p>{{ progress.message }}</p>
</div>
</div>
</template>
<!-- Composable for reusable event listening -->
<script lang="ts">
// src/composables/useEvent.ts
import { onMounted, onUnmounted } from "vue";
import { listen, UnlistenFn } from "@tauri-apps/api/event";
export function useEvent<T>(
eventName: string,
handler: (payload: T) => void
) {
let unlisten: UnlistenFn;
onMounted(async () => {
unlisten = await listen<T>(eventName, (event) => {
handler(event.payload);
});
});
onUnmounted(() => {
if (unlisten) unlisten();
});
}
// Usage
import { ref } from "vue";
import { useEvent } from "@/composables/useEvent";
const stats = ref(null);
useEvent("system-stats", (payload) => {
stats.value = payload;
});
</script>Built-in System Events
Tauri provides built-in events for window lifecycle, focus changes, and app state enabling reactive responses to system-level changes maintaining native desktop experience.
// Frontend: Listening to system events
import { listen } from "@tauri-apps/api/event";
import { appWindow } from "@tauri-apps/api/window";
// Window focus events
await listen("tauri://focus", () => {
console.log("Window gained focus");
});
await listen("tauri://blur", () => {
console.log("Window lost focus");
});
// Window resize
await appWindow.onResized(({ payload }) => {
console.log("Window resized:", payload.width, payload.height);
});
// Window move
await appWindow.onMoved(({ payload }) => {
console.log("Window moved:", payload.x, payload.y);
});
// Window close requested
await appWindow.onCloseRequested(async (event) => {
const confirmed = confirm("Are you sure you want to close?");
if (!confirmed) {
event.preventDefault();
}
});
// File drop
await appWindow.onFileDropEvent((event) => {
if (event.payload.type === "hover") {
console.log("Files hovering:", event.payload.paths);
} else if (event.payload.type === "drop") {
console.log("Files dropped:", event.payload.paths);
} else if (event.payload.type === "cancel") {
console.log("File drop cancelled");
}
});
// Theme change (system dark/light mode)
await listen("tauri://theme-changed", ({ payload }) => {
console.log("System theme changed to:", payload);
});
// React component using system events
function WindowStateTracker() {
const [focused, setFocused] = useState(true);
const [size, setSize] = useState({ width: 800, height: 600 });
useEffect(() => {
const listeners: UnlistenFn[] = [];
(async () => {
listeners.push(
await listen("tauri://focus", () => setFocused(true))
);
listeners.push(
await listen("tauri://blur", () => setFocused(false))
);
listeners.push(
await appWindow.onResized(({ payload }) => {
setSize({ width: payload.width, height: payload.height });
})
);
})();
return () => {
listeners.forEach((unlisten) => unlisten && unlisten());
};
}, []);
return (
<div>
<p>Window {focused ? "focused" : "not focused"}</p>
<p>Size: {size.width} x {size.height}</p>
</div>
);
}Creating Custom Event Types
Define custom event types with TypeScript interfaces and Rust structs ensuring type safety across the event boundary maintaining data integrity and enabling IDE autocompletion.
// TypeScript: Define event types
export enum AppEvents {
UserLoggedIn = "user:logged-in",
UserLoggedOut = "user:logged-out",
DataUpdated = "data:updated",
TaskCompleted = "task:completed",
ErrorOccurred = "error:occurred",
}
export interface UserLoginEvent {
user_id: number;
username: string;
timestamp: string;
}
export interface DataUpdateEvent {
entity: string;
action: "created" | "updated" | "deleted";
data: any;
}
export interface TaskCompletionEvent {
task_id: string;
success: boolean;
result?: string;
error?: string;
}
export interface ErrorEvent {
code: string;
message: string;
stack?: string;
}
// Type-safe event emitter wrapper
class EventEmitter {
static async emit<T>(event: AppEvents, payload: T): Promise<void> {
const { emit } = await import("@tauri-apps/api/event");
await emit(event, payload);
}
static async listen<T>(
event: AppEvents,
handler: (payload: T) => void
): Promise<UnlistenFn> {
const { listen } = await import("@tauri-apps/api/event");
return await listen<T>(event, (e) => handler(e.payload));
}
}
// Usage
import { AppEvents, UserLoginEvent } from "./events";
// Emit typed event
await EventEmitter.emit<UserLoginEvent>(AppEvents.UserLoggedIn, {
user_id: 123,
username: "alice",
timestamp: new Date().toISOString(),
});
// Listen to typed event
await EventEmitter.listen<UserLoginEvent>(
AppEvents.UserLoggedIn,
(payload) => {
console.log(`User ${payload.username} logged in`);
}
);
// Rust: Matching event types
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
pub struct UserLoginEvent {
pub user_id: u32,
pub username: String,
pub timestamp: String,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct DataUpdateEvent {
pub entity: String,
pub action: String,
pub data: serde_json::Value,
}
#[tauri::command]
async fn user_login(window: Window, username: String, user_id: u32) {
let event = UserLoginEvent {
user_id,
username,
timestamp: chrono::Utc::now().to_rfc3339(),
};
window.emit("user:logged-in", event).unwrap();
}
#[tauri::command]
async fn update_data(app: AppHandle, entity: String, action: String, data: serde_json::Value) {
let event = DataUpdateEvent {
entity,
action,
data,
};
app.emit_all("data:updated", event).unwrap();
}Event-Driven Architecture Patterns
// Pattern 1: Event Bus for component communication
class EventBus {
private static listeners = new Map<string, Set<Function>>();
static on(event: string, handler: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
// Return cleanup function
return () => {
this.listeners.get(event)?.delete(handler);
};
}
static emit(event: string, data?: any) {
this.listeners.get(event)?.forEach((handler) => handler(data));
}
}
// Usage
const cleanup = EventBus.on("refresh", () => {
console.log("Refreshing data");
});
EventBus.emit("refresh");
cleanup(); // Remove listener
// Pattern 2: Event Queue for batch processing
class EventQueue {
private queue: any[] = [];
private processing = false;
async add(event: any) {
this.queue.push(event);
if (!this.processing) {
await this.process();
}
}
private async process() {
this.processing = true;
while (this.queue.length > 0) {
const event = this.queue.shift();
await this.handleEvent(event);
}
this.processing = false;
}
private async handleEvent(event: any) {
// Process event
console.log("Processing:", event);
}
}
// Pattern 3: Request-Response with events
class EventRPC {
private static pendingRequests = new Map<string, (value: any) => void>();
static async call<T>(method: string, params: any): Promise<T> {
const requestId = Math.random().toString(36);
return new Promise((resolve) => {
// Store resolver
this.pendingRequests.set(requestId, resolve);
// Emit request
emit("rpc-request", {
id: requestId,
method,
params,
});
// Timeout
setTimeout(() => {
if (this.pendingRequests.has(requestId)) {
this.pendingRequests.delete(requestId);
resolve(null as T);
}
}, 5000);
});
}
static handleResponse(requestId: string, result: any) {
const resolver = this.pendingRequests.get(requestId);
if (resolver) {
resolver(result);
this.pendingRequests.delete(requestId);
}
}
}
// Listen for responses
listen("rpc-response", ({ payload }: any) => {
EventRPC.handleResponse(payload.id, payload.result);
});
// Usage
const result = await EventRPC.call("getUserData", { userId: 123 });
// Pattern 4: Event Aggregator
class EventAggregator {
private buffer: any[] = [];
private timer: NodeJS.Timeout | null = null;
constructor(
private eventName: string,
private batchSize: number = 10,
private timeWindow: number = 1000
) {}
add(data: any) {
this.buffer.push(data);
if (this.buffer.length >= this.batchSize) {
this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.timeWindow);
}
}
private flush() {
if (this.buffer.length > 0) {
emit(this.eventName, { batch: this.buffer });
this.buffer = [];
}
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
}
// Usage
const aggregator = new EventAggregator("analytics-batch", 100, 5000);
aggregator.add({ action: "click", target: "button1" });
aggregator.add({ action: "view", page: "home" });Event System Best Practices
- Always Cleanup: Remove event listeners in component cleanup to prevent memory leaks
- Use Typed Events: Define TypeScript interfaces for event payloads ensuring type safety
- Namespace Events: Use prefixes like
user:ordata:organizing events logically - Avoid Overuse: Use events for notifications, not request-response patterns (use invoke instead)
- Handle Errors: Wrap event emissions in try-catch preventing crashes from failed listeners
- Throttle Emissions: For high-frequency events, throttle or debounce emissions avoiding performance issues
- Document Events: Maintain event catalog documenting all custom events and their payloads
- Test Event Flow: Write tests verifying event emission and handling ensuring reliability
- Monitor Performance: Track listener count and emission frequency identifying bottlenecks
- Use Once Listeners: For one-time events, use
once()instead of manual cleanup
Real-World Example: Live Chat
// Rust: Chat backend with events
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager};
#[derive(Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub id: String,
pub user: String,
pub content: String,
pub timestamp: String,
}
#[tauri::command]
async fn send_message(app: AppHandle, user: String, content: String) -> Result<(), String> {
let message = ChatMessage {
id: uuid::Uuid::new_v4().to_string(),
user,
content,
timestamp: chrono::Utc::now().to_rfc3339(),
};
// Broadcast to all windows
app.emit_all("chat:message", message.clone())
.map_err(|e| e.to_string())?;
// Save to database (simulated)
save_message(&message).await?;
Ok(())
}
#[tauri::command]
async fn typing_indicator(app: AppHandle, user: String, is_typing: bool) {
app.emit_all("chat:typing", (user, is_typing)).ok();
}
async fn save_message(message: &ChatMessage) -> Result<(), String> {
// Database save logic
Ok(())
}
// React: Chat component
import { useState, useEffect, useRef } from "react";
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
interface ChatMessage {
id: string;
user: string;
content: string;
timestamp: string;
}
function ChatApp() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState("");
const [typing, setTyping] = useState<Set<string>>(new Set());
const [currentUser] = useState("Alice");
const typingTimeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
const listeners: UnlistenFn[] = [];
(async () => {
// Listen for new messages
listeners.push(
await listen<ChatMessage>("chat:message", ({ payload }) => {
setMessages((prev) => [...prev, payload]);
})
);
// Listen for typing indicators
listeners.push(
await listen<[string, boolean]>("chat:typing", ({ payload }) => {
const [user, isTyping] = payload;
setTyping((prev) => {
const next = new Set(prev);
if (isTyping) {
next.add(user);
} else {
next.delete(user);
}
return next;
});
})
);
})();
return () => {
listeners.forEach((unlisten) => unlisten && unlisten());
};
}, []);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value);
// Send typing indicator
invoke("typing_indicator", {
user: currentUser,
is_typing: true,
});
// Clear existing timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
// Stop typing after 1 second of inactivity
typingTimeoutRef.current = setTimeout(() => {
invoke("typing_indicator", {
user: currentUser,
is_typing: false,
});
}, 1000);
};
const sendMessage = async () => {
if (!input.trim()) return;
await invoke("send_message", {
user: currentUser,
content: input,
});
setInput("");
// Clear typing indicator
invoke("typing_indicator", {
user: currentUser,
is_typing: false,
});
};
return (
<div className="chat-app">
<div className="messages">
{messages.map((msg) => (
<div key={msg.id} className="message">
<strong>{msg.user}:</strong> {msg.content}
<small>{new Date(msg.timestamp).toLocaleTimeString()}</small>
</div>
))}
</div>
{typing.size > 0 && (
<div className="typing-indicator">
{Array.from(typing).join(", ")} is typing...
</div>
)}
<div className="input-area">
<input
value={input}
onChange={handleInputChange}
onKeyPress={(e) => e.key === "Enter" && sendMessage()}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
}Next Steps
- State Management: Learn sharing data across app
- Window Management: Implement multi-window features
- File System: Build file operations
- System Tray: Create background applications
- Notifications: Add desktop notifications
- Security: Review security best practices
Conclusion
Mastering Tauri's event system enables building responsive, real-time desktop applications with bidirectional communication where backend pushes updates to frontend, multiple windows coordinate seamlessly, and UI responds instantly to state changes without polling creating native-like experiences users expect from professional desktop software. Events complement the invoke API providing push-based messaging perfect for notifications, progress tracking, live data streaming, and inter-window coordination while maintaining clean architecture through decoupled components communicating via well-defined event contracts. Understanding event patterns including proper listener cleanup preventing memory leaks, typed events ensuring data integrity, system events responding to window lifecycle, custom events implementing domain logic, and architectural patterns like pub/sub and event aggregation establishes foundation for sophisticated event-driven applications handling complex workflows with maintainable code. Your Tauri applications now possess powerful real-time capabilities enabling features like live chat, collaborative editing, progress monitoring, system integration, and multi-window coordination delivering professional desktop experiences!
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


