Tauri Advanced Event System
Event Fundamentals
Backend → Frontend Events
Basic event emission:
use tauri::Window;
#[tauri::command] async fn start_download( url: String, window: Window, ) -> Result<(), String> { window.emit("download-started", url) .map_err(|e| e.to_string())?;
// Perform download...
window.emit("download-complete", "Success")
.map_err(|e| e.to_string())
}
Frontend listener:
import { listen, UnlistenFn } from '@tauri-apps/api/event';
const unlisten = await listen<string>('download-started', (event) => { console.log('Download started:', event.payload); });
// Clean up when done unlisten();
Structured Event Payloads
Typed Events with Serde
Backend:
use serde::Serialize;
#[derive(Serialize, Clone)] struct ProgressEvent { current: usize, total: usize, percentage: f64, message: String, speed_mbps: Option<f64>, }
#[tauri::command] async fn download_file( url: String, window: Window, ) -> Result<(), String> { let total_size = get_file_size(&url).await?;
for chunk in 0..total_size {
// Download chunk...
let progress = ProgressEvent {
current: chunk,
total: total_size,
percentage: (chunk as f64 / total_size as f64) * 100.0,
message: format!("Downloading... {}/{}", chunk, total_size),
speed_mbps: Some(calculate_speed()),
};
window.emit("download-progress", progress)
.map_err(|e| e.to_string())?;
}
Ok(())
}
Frontend:
interface ProgressEvent { current: number; total: number; percentage: number; message: string; speed_mbps?: number; }
const unlisten = await listen<ProgressEvent>('download-progress', (event) => { const { current, total, percentage, message, speed_mbps } = event.payload;
updateProgressBar(percentage);
updateStatus(message);
if (speed_mbps) {
updateSpeed(speed_mbps);
}
});
Complex Event Payloads
#[derive(Serialize, Clone)] #[serde(tag = "type", content = "data")] enum AppEvent { UserLoggedIn { user_id: String, username: String }, UserLoggedOut { user_id: String }, DataSynced { items_count: usize, timestamp: String }, ErrorOccurred { code: String, message: String, recoverable: bool }, }
#[tauri::command] async fn perform_login( username: String, password: String, window: Window, ) -> Result<String, String> { let user = authenticate(&username, &password).await?;
// Emit structured event
window.emit("app-event", AppEvent::UserLoggedIn {
user_id: user.id.clone(),
username: user.username.clone(),
}).map_err(|e| e.to_string())?;
Ok(user.id)
}
Frontend:
type AppEvent = | { type: 'UserLoggedIn'; data: { user_id: string; username: string } } | { type: 'UserLoggedOut'; data: { user_id: string } } | { type: 'DataSynced'; data: { items_count: number; timestamp: string } } | { type: 'ErrorOccurred'; data: { code: string; message: string; recoverable: boolean } };
listen<AppEvent>('app-event', (event) => { const appEvent = event.payload;
switch (appEvent.type) {
case 'UserLoggedIn':
handleLogin(appEvent.data.user_id, appEvent.data.username);
break;
case 'UserLoggedOut':
handleLogout(appEvent.data.user_id);
break;
case 'DataSynced':
showSyncSuccess(appEvent.data.items_count);
break;
case 'ErrorOccurred':
handleError(appEvent.data);
break;
}
});
Streaming Data Patterns
Real-Time Data Stream
#[tauri::command] async fn stream_sensor_data( sensor_id: String, window: Window, ) -> Result<(), String> { let mut interval = tokio::time::interval(Duration::from_millis(100));
for _ in 0..100 {
interval.tick().await;
let reading = read_sensor(&sensor_id).await?;
window.emit("sensor-reading", reading)
.map_err(|e| e.to_string())?;
}
window.emit("sensor-stream-ended", sensor_id)
.map_err(|e| e.to_string())
}
Frontend with React:
import { useEffect, useState } from 'react'; import { listen } from '@tauri-apps/api/event';
interface SensorReading { value: number; timestamp: number; unit: string; }
function SensorMonitor() { const [readings, setReadings] = useState<SensorReading[]>([]);
useEffect(() => {
let unlisten: UnlistenFn | undefined;
listen<SensorReading>('sensor-reading', (event) => {
setReadings(prev => [...prev.slice(-99), event.payload]);
}).then(fn => unlisten = fn);
return () => unlisten?.();
}, []);
return (
<div>
{readings.map((r, i) => (
<div key={i}>{r.value} {r.unit}</div>
))}
</div>
);
}
Buffered Streaming
#[tauri::command] async fn stream_logs( log_file: String, window: Window, ) -> Result<(), String> { use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::fs::File;
let file = File::open(log_file).await
.map_err(|e| e.to_string())?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
let mut buffer = Vec::new();
while let Some(line) = lines.next_line().await
.map_err(|e| e.to_string())? {
buffer.push(line);
// Send in batches of 10 lines
if buffer.len() >= 10 {
window.emit("log-batch", buffer.clone())
.map_err(|e| e.to_string())?;
buffer.clear();
}
}
// Send remaining lines
if !buffer.is_empty() {
window.emit("log-batch", buffer)
.map_err(|e| e.to_string())?;
}
Ok(())
}
Multi-Window Communication
Broadcasting to All Windows
use tauri::{AppHandle, Manager};
#[tauri::command] async fn broadcast_message( message: String, app: AppHandle, ) -> Result<(), String> { // Emit to ALL windows app.emit_all("broadcast", message) .map_err(|e| e.to_string()) }
Targeted Window Messaging
#[tauri::command] async fn send_to_window( target_window: String, message: String, app: AppHandle, ) -> Result<(), String> { // Get specific window if let Some(window) = app.get_window(&target_window) { window.emit("private-message", message) .map_err(|e| e.to_string())?; Ok(()) } else { Err(format!("Window '{}' not found", target_window)) } }
Window-to-Window via Backend
Window A (sender):
import { invoke } from '@tauri-apps/api/core';
async function sendToSettings(data: any) { await invoke('relay_to_settings', { data }); }
Backend relay:
#[tauri::command] async fn relay_to_settings( data: serde_json::Value, app: AppHandle, ) -> Result<(), String> { if let Some(settings_window) = app.get_window("settings") { settings_window.emit("data-update", data) .map_err(|e| e.to_string())?; } Ok(()) }
Window B (receiver - settings):
import { listen } from '@tauri-apps/api/event';
useEffect(() => { let unlisten: UnlistenFn | undefined;
listen('data-update', (event) => {
console.log('Received from main window:', event.payload);
updateSettings(event.payload);
}).then(fn => unlisten = fn);
return () => unlisten?.();
}, []);
Frontend → Backend Events
Custom Frontend Events
import { emit } from '@tauri-apps/api/event';
// Frontend emits event await emit('user-action', { action: 'button-click', button_id: 'save-button', timestamp: Date.now() });
Backend listener:
use tauri::{Manager, Listener};
fn main() { tauri::Builder::default() .setup(|app| { let app_handle = app.handle();
// Listen for frontend events
app_handle.listen_global("user-action", move |event| {
if let Some(payload) = event.payload() {
println!("User action: {}", payload);
// Process event...
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Advanced Listener Management
React Hook for Events
import { useEffect, useState } from 'react'; import { listen, UnlistenFn } from '@tauri-apps/api/event';
function useEvent<T>(eventName: string): T | null { const [payload, setPayload] = useState<T | null>(null);
useEffect(() => {
let unlisten: UnlistenFn | undefined;
listen<T>(eventName, (event) => {
setPayload(event.payload);
}).then(fn => unlisten = fn);
return () => unlisten?.();
}, [eventName]);
return payload;
}
// Usage function ProgressDisplay() { const progress = useEvent<ProgressEvent>('download-progress');
if (!progress) return null;
return (
<div>
Progress: {progress.percentage.toFixed(2)}%
</div>
);
}
Event Queue Pattern
import { listen } from '@tauri-apps/api/event';
class EventQueue<T> { private queue: T[] = []; private unlisten?: UnlistenFn;
async start(eventName: string) {
this.unlisten = await listen<T>(eventName, (event) => {
this.queue.push(event.payload);
});
}
dequeue(): T | undefined {
return this.queue.shift();
}
clear() {
this.queue = [];
}
stop() {
this.unlisten?.();
}
get length() {
return this.queue.length;
}
}
// Usage const progressQueue = new EventQueue<ProgressEvent>(); await progressQueue.start('download-progress');
// Process queue periodically setInterval(() => { while (progressQueue.length > 0) { const event = progressQueue.dequeue(); processProgress(event); } }, 100);
One-Time Events
import { once } from '@tauri-apps/api/event';
// Listen for event only once await once<string>('initialization-complete', (event) => { console.log('App initialized:', event.payload); startApp(); });
Error Handling in Events
Safe Event Emission
async fn emit_safe(window: &Window, event: &str, payload: impl Serialize) -> Result<(), String> { window.emit(event, payload) .map_err(|e| { eprintln!("Failed to emit event '{}': {}", event, e); e.to_string() }) }
#[tauri::command] async fn process_with_events( window: Window, ) -> Result<(), String> { emit_safe(&window, "processing-started", "Starting...") .await?;
// Process...
emit_safe(&window, "processing-complete", "Done!")
.await?;
Ok(())
}
Performance Considerations
Throttling Events
use std::time::{Duration, Instant};
#[tauri::command] async fn high_frequency_updates( window: Window, ) -> Result<(), String> { let mut last_emit = Instant::now(); let throttle_duration = Duration::from_millis(100);
for i in 0..10000 {
let value = compute_value(i);
// Only emit every 100ms
if last_emit.elapsed() >= throttle_duration {
window.emit("update", value)
.map_err(|e| e.to_string())?;
last_emit = Instant::now();
}
}
Ok(())
}
Batching Events
#[tauri::command] async fn batch_updates( window: Window, ) -> Result<(), String> { let mut batch = Vec::new();
for item in process_items() {
batch.push(item);
// Emit in batches of 50
if batch.len() >= 50 {
window.emit("batch-update", batch.clone())
.map_err(|e| e.to_string())?;
batch.clear();
}
}
// Emit remaining items
if !batch.is_empty() {
window.emit("batch-update", batch)
.map_err(|e| e.to_string())?;
}
Ok(())
}
Best Practices
-
Always clean up listeners - Use unlisten() to prevent memory leaks
-
Type event payloads - Define interfaces for type safety
-
Use structured events - Tagged unions for multiple event types
-
Throttle high-frequency events - Prevent overwhelming frontend
-
Batch when possible - Reduce serialization overhead
-
Handle errors gracefully - Log failed emissions, don't crash
-
Use once() for one-time events - Initialization, completion signals
-
Namespace event names - Use prefixes like "download:", "user:", "system:"
Common Pitfalls
❌ Forgetting to unlisten:
// WRONG - memory leak function Component() { listen('my-event', handler); // Never cleaned up! }
// CORRECT function Component() { useEffect(() => { let unlisten: UnlistenFn | undefined; listen('my-event', handler).then(fn => unlisten = fn); return () => unlisten?.(); }, []); }
❌ Not handling serialization errors:
// WRONG - struct can't serialize #[derive(Clone)] // Missing Serialize! struct Event { }
window.emit("event", Event {}); // Runtime error!
// CORRECT #[derive(Serialize, Clone)] struct Event { }
❌ Emitting too frequently:
// WRONG - 10000 events in quick succession for i in 0..10000 { window.emit("update", i); // Overwhelming! }
// CORRECT - throttle or batch
Summary
-
Events are async - Backend → Frontend communication
-
Always type payloads - Use serde::Serialize + TypeScript interfaces
-
Clean up listeners - Call unlisten() in cleanup
-
Throttle/batch - High-frequency events need rate limiting
-
Use structured payloads - Tagged unions for multiple event types
-
Window targeting - emit() for specific, emit_all() for broadcast
-
Frontend events - Use emit() from frontend, listen in backend setup