listening-to-tauri-events について
Tauri v2 のフロントエンド開発者向けに、Rust が発行するイベントの購読方法を解説します。グローバル・Webview 向けのリスナー、型付きペイロード、一度限りの購読、後処理を扱います。
主な機能
- 01グローバルイベントの購読
- 02Webview 専用リスナー
- 03型付きイベントペイロード
- 04一度限りのリスナー
- 05リスナーの手動解除
- 06Rust からのイベント発行例
活用シーン
- 01Rust バックエンドの更新受信
- 02特定ウィンドウ向けイベントの処理
- 03SPA コンポーネントでのリスナー解除
- 04TypeScript のイベントデータ型の指定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dchuk/ claude-code-tauri-skills --skill listening-to-tauri-eventsインストール後、Claude Code で/listening-to-tauri-eventsを入力して読み込まれたか確認します。
詳細
Tauri のフロントエンドでイベントをリッスンする
このスキルでは、@tauri-apps/api/event モジュールを使って、Tauri v2 アプリケーションで Rust から発行されたイベントをサブスクライブする方法を説明します。
基本概念
Tauri には 2 種類のイベントスコープがあります。
- グローバルイベント - すべての WebView のリスナーにブロードキャストされます
- WebView 固有のイベント - 特定の WebView ウィンドウを対象とします
重要: WebView 固有のイベントは、グローバルリスナーでは受信されません。Rust からのイベントの発行方法に応じて、適切なリスナータイプを使用してください。
インストール
イベント API は Tauri のコア API パッケージに含まれています。
npm install @tauri-apps/api
# or
pnpm add @tauri-apps/api
# or
yarn add @tauri-apps/api
グローバルイベントのリッスン
基本的なリッスンのパターン
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();
型付きイベントペイロード
Rust のペイロード構造体に対応する 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<T> オブジェクトを受け取ります。
interface Event<T> {
/** Event name */
event: string;
/** Event identifier */
id: number;
/** Event payload (your custom type) */
payload: T;
}
WebView 固有のイベントのリッスン
特定の WebView を対象とする emit_to または emit_filter で発行されたイベントには、次を使用します。
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);
}
});
一度だけのイベントリスナー
一度だけ処理するイベントには once を使用します。
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');
});
クリーンアップとサブスクリプション解除
手動クリーンアップ
不要になったリスナーは、必ずサブスクリプションを解除してください。
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('my-event', (event) => {
// Handle event
});
// Later, when done listening
unlisten();
React コンポーネントのクリーンアップ
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 のクリーンアップ
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 のクリーンアップ
<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>
リスナーの自動クリーンアップ
Tauri は次の状況でリスナーを自動的にクリアします。
- ページの再読み込み - すべてのリスナーがクリアされます
- URL ナビゲーション - リスナーがクリアされます(SPA ルーターを除く)
警告: ページ全体の再読み込みを発生させないシングルページアプリケーションのルーターでは、リスナーは自動的にクリーンアップされません。手動でサブスクリプションを解除する必要があります。
複数のイベントリスナー
複数のイベントをリッスンする
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();
型定義のリファレンス
型付きイベントヘルパーの作成
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);
});
対応する Rust のイベント発行コード
参考として、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 固有のイベントの発行
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();
}
フィルター付きイベントの発行
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();
}
ベストプラクティス
-
コンパイル時に型の不一致を検出できるよう、イベントペイロードには必ず TypeScript インターフェースを定義してください
-
JavaScript の命名規則に合わせるため、Rust の構造体では
serde(rename_all = "camelCase")を使用してください -
メモリーリークを防ぐため、サブスクリプション解除関数を保存し、クリーンアップ時に呼び出してください
-
一度だけ発生する初期化イベントには
onceを使用してください -
リスナーのスコープをイベント発行のスコープに合わせてください。
emit_toイベントには WebView 固有のリスナーを使用してください -
一貫性を保つため、イベントの型定義を共有モジュールに集約してください
よくあるパターン
イベントバスパターン
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 フックパターン
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>;
}