listening-to-tauri-events

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Listening to Tauri Events in the Frontend

在前端监听Tauri事件

This skill covers how to subscribe to events emitted from Rust in a Tauri v2 application using the
@tauri-apps/api/event
module.
本技能介绍如何在Tauri v2应用中使用
@tauri-apps/api/event
模块订阅Rust端发射的事件。

Core Concepts

核心概念

Tauri provides two event scopes:
  1. Global events - Broadcast to all listeners across all webviews
  2. Webview-specific events - Targeted to specific webview windows
Important: Webview-specific events are NOT received by global listeners. Use the appropriate listener type based on how events are emitted from Rust.
Tauri提供两种事件作用域:
  1. 全局事件 - 广播到所有webview中的所有监听器
  2. Webview专属事件 - 定向到特定webview窗口
注意: Webview专属事件不会被全局监听器接收,请根据Rust端的事件发射方式选择合适的监听器类型。

Installation

安装

The event API is part of the core Tauri API package:
bash
npm install @tauri-apps/api
事件API是Tauri核心API包的一部分:
bash
npm install @tauri-apps/api

or

or

pnpm add @tauri-apps/api
pnpm add @tauri-apps/api

or

or

yarn add @tauri-apps/api
undefined
yarn add @tauri-apps/api
undefined

Global Event Listening

全局事件监听

Basic Listen Pattern

基础监听模式

typescript
import { listen } from '@tauri-apps/api/event';

// Listen for a global event
const unlisten = await listen('download-started', (event) => {
  console.log('Event received:', event.payload);
});

// Clean up when done
unlisten();
typescript
import { listen } from '@tauri-apps/api/event';

// Listen for a global event
const unlisten = await listen('download-started', (event) => {
  console.log('Event received:', event.payload);
});

// Clean up when done
unlisten();

Typed Event Payloads

类型化事件负载

Define TypeScript interfaces matching your Rust payload structures:
typescript
import { listen } from '@tauri-apps/api/event';

// Define the payload type (matches Rust struct with camelCase)
interface DownloadStarted {
  url: string;
  downloadId: number;
  contentLength: number;
}

// Use generic type parameter for type safety
const unlisten = await listen<DownloadStarted>('download-started', (event) => {
  console.log(`Downloading from ${event.payload.url}`);
  console.log(`Content length: ${event.payload.contentLength} bytes`);
  console.log(`Download ID: ${event.payload.downloadId}`);
});
定义与Rust端负载结构匹配的TypeScript接口:
typescript
import { listen } from '@tauri-apps/api/event';

// Define the payload type (matches Rust struct with camelCase)
interface DownloadStarted {
  url: string;
  downloadId: number;
  contentLength: number;
}

// Use generic type parameter for type safety
const unlisten = await listen<DownloadStarted>('download-started', (event) => {
  console.log(`Downloading from ${event.payload.url}`);
  console.log(`Content length: ${event.payload.contentLength} bytes`);
  console.log(`Download ID: ${event.payload.downloadId}`);
});

Event Object Structure

事件对象结构

The event callback receives an
Event<T>
object:
typescript
interface Event<T> {
  /** Event name */
  event: string;
  /** Event identifier */
  id: number;
  /** Event payload (your custom type) */
  payload: T;
}
事件回调会接收一个
Event<T>
对象:
typescript
interface Event<T> {
  /** Event name */
  event: string;
  /** Event identifier */
  id: number;
  /** Event payload (your custom type) */
  payload: T;
}

Webview-Specific Event Listening

Webview专属事件监听

For events emitted with
emit_to
or
emit_filter
targeting specific webviews:
typescript
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

const appWebview = getCurrentWebviewWindow();

// Listen for events targeted to this specific webview
const unlisten = await appWebview.listen<string>('login-result', (event) => {
  if (event.payload === 'loggedIn') {
    localStorage.setItem('authenticated', 'true');
  } else {
    console.error('Login failed:', event.payload);
  }
});
对于使用
emit_to
emit_filter
定向到特定webview的事件:
typescript
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

const appWebview = getCurrentWebviewWindow();

// Listen for events targeted to this specific webview
const unlisten = await appWebview.listen<string>('login-result', (event) => {
  if (event.payload === 'loggedIn') {
    localStorage.setItem('authenticated', 'true');
  } else {
    console.error('Login failed:', event.payload);
  }
});

One-Time Event Listeners

一次性事件监听器

Use
once
for events that should only be handled a single time:
typescript
import { once } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

// Global one-time listener
await once('app-ready', (event) => {
  console.log('Application is ready');
});

// Webview-specific one-time listener
const appWebview = getCurrentWebviewWindow();
await appWebview.once('initialized', (event) => {
  console.log('Webview initialized');
});
使用
once
处理只需触发一次的事件:
typescript
import { once } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

// Global one-time listener
await once('app-ready', (event) => {
  console.log('Application is ready');
});

// Webview-specific one-time listener
const appWebview = getCurrentWebviewWindow();
await appWebview.once('initialized', (event) => {
  console.log('Webview initialized');
});

Cleanup and Unsubscription

清理与取消订阅

Manual Cleanup

手动清理

Always unsubscribe listeners when they are no longer needed:
typescript
import { listen } from '@tauri-apps/api/event';

const unlisten = await listen('my-event', (event) => {
  // Handle event
});

// Later, when done listening
unlisten();
不再需要监听器时务必取消订阅:
typescript
import { listen } from '@tauri-apps/api/event';

const unlisten = await listen('my-event', (event) => {
  // Handle event
});

// Later, when done listening
unlisten();

React Component Cleanup

React组件清理

typescript
import { useEffect } from 'react';
import { listen } from '@tauri-apps/api/event';

interface ProgressPayload {
  percent: number;
  message: string;
}

function DownloadProgress() {
  useEffect(() => {
    let unlisten: (() => void) | undefined;

    const setupListener = async () => {
      unlisten = await listen<ProgressPayload>('download-progress', (event) => {
        console.log(`Progress: ${event.payload.percent}%`);
      });
    };

    setupListener();

    // Cleanup when component unmounts
    return () => {
      if (unlisten) {
        unlisten();
      }
    };
  }, []);

  return <div>Listening for progress...</div>;
}
typescript
import { useEffect } from 'react';
import { listen } from '@tauri-apps/api/event';

interface ProgressPayload {
  percent: number;
  message: string;
}

function DownloadProgress() {
  useEffect(() => {
    let unlisten: (() => void) | undefined;

    const setupListener = async () => {
      unlisten = await listen<ProgressPayload>('download-progress', (event) => {
        console.log(`Progress: ${event.payload.percent}%`);
      });
    };

    setupListener();

    // Cleanup when component unmounts
    return () => {
      if (unlisten) {
        unlisten();
      }
    };
  }, []);

  return <div>Listening for progress...</div>;
}

Vue Composition API Cleanup

Vue组合式API清理

typescript
import { onMounted, onUnmounted } from 'vue';
import { listen } from '@tauri-apps/api/event';

interface NotificationPayload {
  title: string;
  body: string;
}

export default {
  setup() {
    let unlisten: (() => void) | undefined;

    onMounted(async () => {
      unlisten = await listen<NotificationPayload>('notification', (event) => {
        console.log(`${event.payload.title}: ${event.payload.body}`);
      });
    });

    onUnmounted(() => {
      if (unlisten) {
        unlisten();
      }
    });
  }
};
typescript
import { onMounted, onUnmounted } from 'vue';
import { listen } from '@tauri-apps/api/event';

interface NotificationPayload {
  title: string;
  body: string;
}

export default {
  setup() {
    let unlisten: (() => void) | undefined;

    onMounted(async () => {
      unlisten = await listen<NotificationPayload>('notification', (event) => {
        console.log(`${event.payload.title}: ${event.payload.body}`);
      });
    });

    onUnmounted(() => {
      if (unlisten) {
        unlisten();
      }
    });
  }
};

Svelte Cleanup

Svelte清理

svelte
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { listen } from '@tauri-apps/api/event';

  interface StatusPayload {
    status: 'idle' | 'loading' | 'complete';
  }

  let unlisten: (() => void) | undefined;

  onMount(async () => {
    unlisten = await listen<StatusPayload>('status-change', (event) => {
      console.log('Status:', event.payload.status);
    });
  });

  onDestroy(() => {
    if (unlisten) {
      unlisten();
    }
  });
</script>
svelte
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { listen } from '@tauri-apps/api/event';

  interface StatusPayload {
    status: 'idle' | 'loading' | 'complete';
  }

  let unlisten: (() => void) | undefined;

  onMount(async () => {
    unlisten = await listen<StatusPayload>('status-change', (event) => {
      console.log('Status:', event.payload.status);
    });
  });

  onDestroy(() => {
    if (unlisten) {
      unlisten();
    }
  });
</script>

Automatic Listener Cleanup

自动监听器清理

Tauri automatically clears listeners in these scenarios:
  • Page reload - All listeners are cleared
  • URL navigation - Listeners are cleared (except in SPA routers)
Warning: Single Page Application routers that do not trigger full page reloads will NOT automatically clean up listeners. You must manually unsubscribe.
在以下场景中,Tauri会自动清除监听器:
  • 页面重载 - 所有监听器都会被清除
  • URL导航 - 监听器被清除(SPA路由除外)
警告: 不会触发全页重载的单页应用路由不会自动清理监听器,你必须手动取消订阅。

Multiple Event Listeners

多事件监听器

Listening to Multiple Events

监听多个事件

typescript
import { listen } from '@tauri-apps/api/event';

interface DownloadEvent {
  url: string;
}

interface ProgressEvent {
  percent: number;
}

interface CompleteEvent {
  path: string;
  size: number;
}

async function setupDownloadListeners() {
  const unlisteners: Array<() => void> = [];

  unlisteners.push(
    await listen<DownloadEvent>('download-started', (event) => {
      console.log(`Started downloading: ${event.payload.url}`);
    })
  );

  unlisteners.push(
    await listen<ProgressEvent>('download-progress', (event) => {
      console.log(`Progress: ${event.payload.percent}%`);
    })
  );

  unlisteners.push(
    await listen<CompleteEvent>('download-complete', (event) => {
      console.log(`Complete: ${event.payload.path} (${event.payload.size} bytes)`);
    })
  );

  // Return cleanup function
  return () => {
    unlisteners.forEach((unlisten) => unlisten());
  };
}

// Usage
const cleanup = await setupDownloadListeners();
// Later...
cleanup();
typescript
import { listen } from '@tauri-apps/api/event';

interface DownloadEvent {
  url: string;
}

interface ProgressEvent {
  percent: number;
}

interface CompleteEvent {
  path: string;
  size: number;
}

async function setupDownloadListeners() {
  const unlisteners: Array<() => void> = [];

  unlisteners.push(
    await listen<DownloadEvent>('download-started', (event) => {
      console.log(`Started downloading: ${event.payload.url}`);
    })
  );

  unlisteners.push(
    await listen<ProgressEvent>('download-progress', (event) => {
      console.log(`Progress: ${event.payload.percent}%`);
    })
  );

  unlisteners.push(
    await listen<CompleteEvent>('download-complete', (event) => {
      console.log(`Complete: ${event.payload.path} (${event.payload.size} bytes)`);
    })
  );

  // Return cleanup function
  return () => {
    unlisteners.forEach((unlisten) => unlisten());
  };
}

// Usage
const cleanup = await setupDownloadListeners();
// Later...
cleanup();

Type Definitions Reference

类型定义参考

Creating Typed Event Helpers

创建类型化事件助手

typescript
import { listen, once, Event } from '@tauri-apps/api/event';

// Define all your event types in one place
export interface AppEvents {
  'download-started': { url: string; downloadId: number };
  'download-progress': { downloadId: number; percent: number };
  'download-complete': { downloadId: number; path: string };
  'download-error': { downloadId: number; error: string };
  'notification': { title: string; body: string };
}

// Type-safe listen helper
export async function listenTyped<K extends keyof AppEvents>(
  eventName: K,
  handler: (event: Event<AppEvents[K]>) => void
): Promise<() => void> {
  return listen<AppEvents[K]>(eventName, handler);
}

// Usage
const unlisten = await listenTyped('download-started', (event) => {
  // event.payload is typed as { url: string; downloadId: number }
  console.log(event.payload.url);
});
typescript
import { listen, once, Event } from '@tauri-apps/api/event';

// Define all your event types in one place
export interface AppEvents {
  'download-started': { url: string; downloadId: number };
  'download-progress': { downloadId: number; percent: number };
  'download-complete': { downloadId: number; path: string };
  'download-error': { downloadId: number; error: string };
  'notification': { title: string; body: string };
}

// Type-safe listen helper
export async function listenTyped<K extends keyof AppEvents>(
  eventName: K,
  handler: (event: Event<AppEvents[K]>) => void
): Promise<() => void> {
  return listen<AppEvents[K]>(eventName, handler);
}

// Usage
const unlisten = await listenTyped('download-started', (event) => {
  // event.payload is typed as { url: string; downloadId: number }
  console.log(event.payload.url);
});

Corresponding Rust Emit Code

对应的Rust发射代码

For reference, here is how events are emitted from Rust:
作为参考,以下是Rust端如何发射事件:

Global Emit

全局发射

rust
use tauri::{AppHandle, Emitter};
use serde::Serialize;

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DownloadStarted {
    url: String,
    download_id: usize,
    content_length: usize,
}

#[tauri::command]
fn start_download(app: AppHandle, url: String) {
    app.emit("download-started", DownloadStarted {
        url,
        download_id: 1,
        content_length: 1024,
    }).unwrap();
}
rust
use tauri::{AppHandle, Emitter};
use serde::Serialize;

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DownloadStarted {
    url: String,
    download_id: usize,
    content_length: usize,
}

#[tauri::command]
fn start_download(app: AppHandle, url: String) {
    app.emit("download-started", DownloadStarted {
        url,
        download_id: 1,
        content_length: 1024,
    }).unwrap();
}

Webview-Specific Emit

Webview专属发射

rust
use tauri::{AppHandle, Emitter};

#[tauri::command]
fn notify_window(app: AppHandle, window_label: &str, message: String) {
    // Emit to a specific webview by label
    app.emit_to(window_label, "notification", message).unwrap();
}
rust
use tauri::{AppHandle, Emitter};

#[tauri::command]
fn notify_window(app: AppHandle, window_label: &str, message: String) {
    // Emit to a specific webview by label
    app.emit_to(window_label, "notification", message).unwrap();
}

Filtered Emit

过滤发射

rust
use tauri::{AppHandle, Emitter, EventTarget};

#[tauri::command]
fn broadcast_to_editors(app: AppHandle, content: String) {
    app.emit_filter("content-update", content, |target| {
        match target {
            EventTarget::WebviewWindow { label } => {
                label.starts_with("editor-")
            }
            _ => false,
        }
    }).unwrap();
}
rust
use tauri::{AppHandle, Emitter, EventTarget};

#[tauri::command]
fn broadcast_to_editors(app: AppHandle, content: String) {
    app.emit_filter("content-update", content, |target| {
        match target {
            EventTarget::WebviewWindow { label } => {
                label.starts_with("editor-")
            }
            _ => false,
        }
    }).unwrap();
}

Best Practices

最佳实践

  1. Always define TypeScript interfaces for event payloads to catch type mismatches at compile time
  2. Use
    serde(rename_all = "camelCase")
    in Rust structs to match JavaScript naming conventions
  3. Store unlisten functions and call them during cleanup to prevent memory leaks
  4. Use
    once
    for initialization events
    that only fire a single time
  5. Match listener scope to emit scope - use webview-specific listeners for
    emit_to
    events
  6. Centralize event type definitions in a shared module for consistency
  1. 始终定义TypeScript接口用于事件负载,在编译时捕获类型不匹配问题
  2. **在Rust结构体中使用
    serde(rename_all = "camelCase")
    **以匹配JavaScript命名规范
  3. 存储取消订阅函数并在清理时调用,防止内存泄漏
  4. **对初始化事件使用
    once
    **这类只会触发一次的事件
  5. 监听器作用域与发射作用域匹配 - 对
    emit_to
    事件使用webview专属监听器
  6. 集中管理事件类型定义在共享模块中以保持一致性

Common Patterns

常见模式

Event Bus Pattern

事件总线模式

typescript
import { listen, Event } from '@tauri-apps/api/event';

type EventHandler<T> = (payload: T) => void;

class TauriEventBus {
  private unlisteners: Map<string, () => void> = new Map();

  async subscribe<T>(event: string, handler: EventHandler<T>): Promise<void> {
    if (this.unlisteners.has(event)) {
      this.unlisteners.get(event)!();
    }

    const unlisten = await listen<T>(event, (e: Event<T>) => {
      handler(e.payload);
    });

    this.unlisteners.set(event, unlisten);
  }

  unsubscribe(event: string): void {
    const unlisten = this.unlisteners.get(event);
    if (unlisten) {
      unlisten();
      this.unlisteners.delete(event);
    }
  }

  unsubscribeAll(): void {
    this.unlisteners.forEach((unlisten) => unlisten());
    this.unlisteners.clear();
  }
}

export const eventBus = new TauriEventBus();
typescript
import { listen, Event } from '@tauri-apps/api/event';

type EventHandler<T> = (payload: T) => void;

class TauriEventBus {
  private unlisteners: Map<string, () => void> = new Map();

  async subscribe<T>(event: string, handler: EventHandler<T>): Promise<void> {
    if (this.unlisteners.has(event)) {
      this.unlisteners.get(event)!();
    }

    const unlisten = await listen<T>(event, (e: Event<T>) => {
      handler(e.payload);
    });

    this.unlisteners.set(event, unlisten);
  }

  unsubscribe(event: string): void {
    const unlisten = this.unlisteners.get(event);
    if (unlisten) {
      unlisten();
      this.unlisteners.delete(event);
    }
  }

  unsubscribeAll(): void {
    this.unlisteners.forEach((unlisten) => unlisten());
    this.unlisteners.clear();
  }
}

export const eventBus = new TauriEventBus();

React Hook Pattern

React Hook模式

typescript
import { useEffect, useState } from 'react';
import { listen, Event } from '@tauri-apps/api/event';

export function useTauriEvent<T>(eventName: string, initialValue: T): T {
  const [value, setValue] = useState<T>(initialValue);

  useEffect(() => {
    let unlisten: (() => void) | undefined;

    listen<T>(eventName, (event: Event<T>) => {
      setValue(event.payload);
    }).then((fn) => {
      unlisten = fn;
    });

    return () => {
      if (unlisten) {
        unlisten();
      }
    };
  }, [eventName]);

  return value;
}

// Usage
function StatusDisplay() {
  const status = useTauriEvent<string>('app-status', 'initializing');
  return <div>Status: {status}</div>;
}
typescript
import { useEffect, useState } from 'react';
import { listen, Event } from '@tauri-apps/api/event';

export function useTauriEvent<T>(eventName: string, initialValue: T): T {
  const [value, setValue] = useState<T>(initialValue);

  useEffect(() => {
    let unlisten: (() => void) | undefined;

    listen<T>(eventName, (event: Event<T>) => {
      setValue(event.payload);
    }).then((fn) => {
      unlisten = fn;
    });

    return () => {
      if (unlisten) {
        unlisten();
      }
    };
  }, [eventName]);

  return value;
}

// Usage
function StatusDisplay() {
  const status = useTauriEvent<string>('app-status', 'initializing');
  return <div>Status: {status}</div>;
}