Advanced Tauri event patterns for bidirectional communication, streaming data, window-to-window messaging, and custom event handling
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
You Might Also Like
Related Skills

verify
Use when you want to validate changes before committing, or when you need to check all React contribution requirements.
facebook
test
Use when you need to run tests for React core. Supports source, www, stable, and experimental channels.
facebook
feature-flags
Use when feature flag tests fail, flags need updating, understanding @gate pragmas, debugging channel-specific test failures, or adding new flags to React.
facebook
extract-errors
Use when adding new error messages to React, or seeing "unknown error code" warnings.
facebook