tauri-event-system

Tauri Advanced Event System

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tauri-event-system" with this command: npx skills add bobmatnyc/claude-mpm-skills/bobmatnyc-claude-mpm-skills-tauri-event-system

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(&#x26;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&#x3C;SensorReading>('sensor-reading', (event) => {
        setReadings(prev => [...prev.slice(-99), event.payload]);
    }).then(fn => unlisten = fn);

    return () => unlisten?.();
}, []);

return (
    &#x3C;div>
        {readings.map((r, i) => (
            &#x3C;div key={i}>{r.value} {r.unit}&#x3C;/div>
        ))}
    &#x3C;/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&#x3C;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 (
    &#x3C;div>
        Progress: {progress.percentage.toFixed(2)}%
    &#x3C;/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&#x3C;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(&#x26;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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

drizzle-orm

No summary provided by upstream source.

Repository SourceNeeds Review
General

pydantic

No summary provided by upstream source.

Repository SourceNeeds Review
General

playwright-e2e-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

tailwind-css

No summary provided by upstream source.

Repository SourceNeeds Review