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
// 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
[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
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
// 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
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
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
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
if (!progress) return null;
return (
<div>
Progress: {progress.percentage.toFixed(2)}%
</div>
);
}
Event Queue Pattern import { listen } from '@tauri-apps/api/event';
class EventQueue
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
// 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
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