# Tauri 2.0 Developer Guide — Part 2: Windows, Plugins, WebSocket, State & Migration > **Version**: 2.0 | **Last Updated**: 2026-05-31 > **Official Docs**: https://v2.tauri.app > > This file covers Sections 8-15 of the Definitive Guide. Part 1 covers Architecture, Config, Capabilities, Commands & IPC. --- ## Table of Contents 8. [Window Management](#8-window-management) 9. [Plugin Ecosystem](#9-plugin-ecosystem) 10. [Native WebSocket Server (Rust-side)](#10-native-websocket-server-rust-side) 11. [Script Injection into Webview Windows](#11-script-injection-into-webview-windows) 12. [State Management](#12-state-management) 13. [v1 to v2 Migration Reference](#13-v1-to-v2-migration-reference) 14. [Common Anti-Patterns to Avoid](#14-common-anti-patterns-to-avoid) 15. [Quick Reference Cheat Sheet](#15-quick-reference-cheat-sheet) **See also**: [Part 1: Core Architecture, Config, Commands & IPC](./01-core-architecture-config-commands-ipc.md) --- ## 8. Window Management ### Creating Windows Dynamically ```rust use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; #[tauri::command] fn open_settings(app_handle: AppHandle) -> Result<(), String> { // Focus existing window if it exists if let Some(window) = app_handle.get_webview_window("settings") { window.set_focus().map_err(|e| e.to_string())?; return Ok(()); } // Create new window let _window = WebviewWindowBuilder::new( &app_handle, "settings", WebviewUrl::App("settings.html".into()) ) .title("Settings") .inner_size(600.0, 400.0) .resizable(true) .center(true) .build() .map_err(|e| e.to_string())?; Ok(()) } ``` ### Creating Windows with External URLs ```rust use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder}; #[tauri::command] async fn open_remote(app_handle: AppHandle, url: String) -> Result { let target_url = url.parse() .map(WebviewUrl::External) .map_err(|_| "Invalid URL".to_string())?; app_handle.run_on_main_thread(move || { WebviewWindowBuilder::new(&app_handle, "remote-window", target_url) .title("Remote Session") .inner_size(1024.0, 768.0) .build() }).map_err(|e| e.to_string())?; Ok("Window spawned".to_string()) } ``` **Key pattern**: Always use `run_on_main_thread` when creating windows from async contexts or non-main-thread code. This is critical — window creation MUST happen on the main thread. ### Window Builder Options ```rust WebviewWindowBuilder::new(&app, "label", WebviewUrl::App("path".into())) .title("Window Title") .inner_size(800.0, 600.0) // Width, Height .min_inner_size(400.0, 300.0) // Minimum size .max_inner_size(1920.0, 1080.0) // Maximum size .resizable(true) .fullscreen(false) .center(true) .decorations(true) // Title bar .always_on_top(false) .visible(true) // Start visible .focused(true) .initialization_script("...") // Inject JS on load .build()?; ``` ### Managing Windows from Frontend ```javascript import { getCurrentWebviewWindow, WebviewWindow } from '@tauri-apps/api/webviewWindow'; // Get current window reference const appWindow = getCurrentWebviewWindow(); // Window operations await appWindow.setTitle("New Title"); await appWindow.center(); await appWindow.minimize(); await appWindow.maximize(); await appWindow.unmaximize(); await appWindow.close(); await appWindow.setFullscreen(true); await appWindow.setResizable(false); await appWindow.setSize({ width: 1024, height: 768 }); await appWindow.setPosition({ x: 100, y: 100 }); // Create new window from frontend const newWindow = new WebviewWindow('secondary', { url: 'index.html#/settings', title: 'Settings', width: 600, height: 400, }); await newWindow.once('tauri://created', () => { console.log('Window created!'); }); await newWindow.once('tauri://error', (e) => { console.error('Window creation error:', e); }); ``` ### Window v1 to v2 API Renames | v1 | v2 | |----|----| | `tauri::Window` | `tauri::WebviewWindow` | | `tauri::WindowBuilder` | `tauri::WebviewWindowBuilder` | | `tauri::WindowUrl` | `tauri::WebviewUrl` | | `app.get_window("main")` | `app.get_webview_window("main")` | | `tauri::WindowUrl::App(path)` | `tauri::WebviewUrl::App(path.into())` | | `WebviewUrl::External(url)` | `WebviewUrl::External(url)` | --- ## 9. Plugin Ecosystem ### Complete List of Official Tauri 2.0 Plugins (33 plugins) | Plugin | Crate | JS Package | Platforms | |--------|-------|-----------|-----------| | Autostart | `tauri-plugin-autostart` | `@tauri-apps/plugin-autostart` | Win, Lin, Mac | | Barcode Scanner | `tauri-plugin-barcode-scanner` | `@tauri-apps/plugin-barcode-scanner` | Android, iOS | | Biometric | `tauri-plugin-biometric` | `@tauri-apps/plugin-biometric` | Android, iOS | | Clipboard Manager | `tauri-plugin-clipboard-manager` | `@tauri-apps/plugin-clipboard-manager` | Win, Lin, Mac, Android, iOS | | CLI | `tauri-plugin-cli` | `@tauri-apps/plugin-cli` | Win, Lin, Mac, Android, iOS | | Deep Linking | `tauri-plugin-deep-link` | `@tauri-apps/plugin-deep-link` | Win, Mac, Android, iOS | | Dialog | `tauri-plugin-dialog` | `@tauri-apps/plugin-dialog` | Win, Lin, Mac, Android | | File System | `tauri-plugin-fs` | `@tauri-apps/plugin-fs` | Win, Lin, Mac, Android, iOS | | Geolocation | `tauri-plugin-geolocation` | `@tauri-apps/plugin-geolocation` | Android, iOS | | Global Shortcut | `tauri-plugin-global-shortcut` | `@tauri-apps/plugin-global-shortcut` | Win, Lin, Mac | | Haptics | `tauri-plugin-haptics` | `@tauri-apps/plugin-haptics` | Android, iOS | | HTTP Client | `tauri-plugin-http` | `@tauri-apps/plugin-http` | Win, Lin, Mac, Android, iOS | | Localhost | `tauri-plugin-localhost` | `@tauri-apps/plugin-localhost` | Win, Lin, Mac | | Logging | `tauri-plugin-log` | `@tauri-apps/plugin-log` | Win, Lin, Mac, Android, iOS | | NFC | `tauri-plugin-nfc` | `@tauri-apps/plugin-nfc` | Android, iOS | | Notification | `tauri-plugin-notification` | `@tauri-apps/plugin-notification` | Win, Lin, Mac, Android | | Opener | `tauri-plugin-opener` | `@tauri-apps/plugin-opener` | Win, Lin, Mac, Android, iOS | | OS Info | `tauri-plugin-os` | `@tauri-apps/plugin-os` | Win, Lin, Mac, Android, iOS | | Persisted Scope | `tauri-plugin-persisted-scope` | `@tauri-apps/plugin-persisted-scope` | Win, Lin, Mac, Android, iOS | | Positioner | `tauri-plugin-positioner` | `@tauri-apps/plugin-positioner` | Win, Lin, Mac | | Process | `tauri-plugin-process` | `@tauri-apps/plugin-process` | Win, Lin, Mac, Android, iOS | | Shell | `tauri-plugin-shell` | `@tauri-apps/plugin-shell` | Win, Lin, Mac, Android | | Single Instance | `tauri-plugin-single-instance` | `@tauri-apps/plugin-single-instance` | Win, Lin, Mac | | SQL | `tauri-plugin-sql` | `@tauri-apps/plugin-sql` | Win, Lin, Mac, Android, iOS | | Store | `tauri-plugin-store` | `@tauri-apps/plugin-store` | Win, Lin, Mac, Android, iOS | | Stronghold | `tauri-plugin-stronghold` | `@tauri-apps/plugin-stronghold` | Win, Lin, Mac, Android, iOS | | System Tray | `tauri-plugin-system-tray` | `@tauri-apps/plugin-system-tray` | Win, Lin, Mac | | Updater | `tauri-plugin-updater` | `@tauri-apps/plugin-updater` | Win, Lin, Mac | | Upload | `tauri-plugin-upload` | `@tauri-apps/plugin-upload` | Win, Lin, Mac, Android, iOS | | Websocket | `tauri-plugin-websocket` | `@tauri-apps/plugin-websocket` | Win, Lin, Mac, Android, iOS | | Window State | `tauri-plugin-window-state` | `@tauri-apps/plugin-window-state` | Win, Lin, Mac, Android, iOS | | Window Customization | `tauri-plugin-window-customization` | `@tauri-apps/plugin-window-customization` | Win, Lin, Mac | ### Adding a Plugin (4-Step Pattern) Every plugin follows the same 4-step setup: **Step 1: Add Rust dependency** ```toml # src-tauri/Cargo.toml [dependencies] tauri-plugin-store = "2" ``` **Step 2: Register in Builder** ```rust // src-tauri/src/lib.rs tauri::Builder::default() .plugin(tauri_plugin_store::Builder::default().build()) ``` **Step 3: Add JS dependency** ```bash npm install @tauri-apps/plugin-store ``` **Step 4 (optional but required): Add permissions** ```json // src-tauri/capabilities/default.json { "permissions": ["store:default"] } ``` ### Plugin Registration Variations Different plugins have different initialization patterns: ```rust // Simple init (most plugins) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_clipboard_manager::init()) // Builder pattern (plugins with configuration) .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_log::Builder::default().build()) // With closures/config .plugin(tauri_plugin_single_instance::init(|app, args, cwd| { if let Some(window) = app.get_webview_window("main") { let _ = window.set_focus(); } })) .plugin(tauri_plugin_shell::init()) ``` ### Key Plugin Examples #### File System Plugin (`tauri-plugin-fs`) Rust side uses `std::fs`. JS side uses the plugin: ```javascript import { readFile, writeFile, mkdir, exists, remove, readDir } from '@tauri-apps/plugin-fs'; // Read a text file const content = await readFile('config.json'); // Write a file await writeFile('output.txt', 'Hello World!'); // Create directory await mkdir('data/logs', { recursive: true }); // Check existence const hasFile = await exists('config.json'); // Remove file await remove('temp.txt'); ``` #### HTTP Client Plugin (`tauri-plugin-http`) ```javascript import { fetch } from '@tauri-apps/plugin-http'; const response = await fetch('https://api.example.com/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: 'value' }), }); const data = await response.json(); ``` #### Store Plugin (Persistent Key-Value) ```javascript import { Store } from '@tauri-apps/plugin-store'; const store = await Store.load('settings.json'); await store.set('theme', 'dark'); await store.set('volume', 75); const theme = await store.get('theme'); // 'dark' await store.save(); ``` #### Dialog Plugin ```javascript import { open, save, message, ask, confirm } from '@tauri-apps/plugin-dialog'; // File picker const file = await open({ multiple: false, filters: [{ name: 'JSON', extensions: ['json'] }] }); // Save dialog const path = await save({ filters: [{ name: 'Text', extensions: ['txt'] }] }); // Alert await message('Operation completed!'); // Confirmation const yes = await confirm('Are you sure you want to delete this file?'); ``` --- ## 10. Native WebSocket Server (Rust-side) This section documents the WebSocket server pattern for Tauri 2.0 applications. This pattern is useful when your app needs to **act as a server** — accepting connections from local webviews, external applications, or other processes. ### Architecture Overview The WebSocket server runs as a Tokio async task spawned during the Tauri `setup` phase. It uses `tokio-tungstenite` for the WebSocket implementation and `tokio::sync::broadcast` for message fan-out to all connected clients. ### Dependencies (`Cargo.toml`) ```toml [dependencies] tokio = { version = "1", features = ["net", "rt", "sync", "macros"] } tokio-tungstenite = "0.24" futures-util = "0.3" ``` ### Server Implementation ```rust // src-tauri/src/server.rs use tokio::net::TcpListener; use tokio_tungstenite::accept_async; use futures_util::stream::StreamExt; use futures_util::sink::SinkExt; use tokio::sync::broadcast; pub async fn start_websocket_server() { let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); println!("WebSocket Server listening on ws://127.0.0.1:8080"); // Broadcast channel: 16 message queue depth let (tx, _rx) = broadcast::channel::(16); while let Ok((stream, _addr)) = listener.accept().await { let tx = tx.clone(); let mut rx = tx.subscribe(); // Spawn a task per client connection tauri::async_runtime::spawn(async move { if let Ok(ws_stream) = accept_async(stream).await { let (mut ws_sender, mut ws_receiver) = ws_stream.split(); loop { tokio::select! { // Branch A: Receive message FROM this client incoming = ws_receiver.next() => { match incoming { Some(Ok(msg)) if msg.is_text() => { let text = msg.to_text().unwrap().to_string(); println!("Received: {}", text); // Broadcast to ALL other connected clients let _ = tx.send(text); } _ => break, // Disconnect } } // Branch B: Receive broadcast FROM other clients broadcast = rx.recv() => { if let Ok(payload) = broadcast { let frame = tokio_tungstenite::tungstenite::Message::Text( payload.into() ); if let Err(e) = ws_sender.send(frame).await { eprintln!("Send error: {}", e); break; } } } } } } }); } } ``` ### Spawning the Server from `lib.rs` ```rust // src-tauri/src/lib.rs mod server; pub fn run() { tauri::Builder::default() .setup(|_app| { tauri::async_runtime::spawn(server::start_websocket_server()); Ok(()) }) // ... } ``` **Key pattern**: Use `tauri::async_runtime::spawn()` to launch the server. This ensures the server runs on Tauri's managed async runtime rather than blocking the setup hook. ### Frontend Client Connection ```javascript // Connect to the local WebSocket server const ws = new WebSocket('ws://127.0.0.1:8080'); ws.onopen = () => { console.log('Connected to local WS server'); ws.send(JSON.stringify({ type: 'hello', from: 'main-window' })); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); console.log('Received from server:', data); }; ws.onerror = (err) => { console.error('WebSocket error:', err); }; ``` ### When to Use This Pattern vs. the WebSocket Plugin | Scenario | Approach | |----------|----------| | App connects to an **external** WebSocket server | Use `tauri-plugin-websocket` (client plugin) | | App **is** the WebSocket server | Use the native Tokio pattern above | | Cross-window communication within the app | Use Tauri events/channels, or this pattern for high throughput | --- ## 11. Script Injection into Webview Windows This section documents the script injection pattern for injecting JavaScript into dynamically created webview windows (including those loading external URLs). ### The Pattern Use `WebviewWindowBuilder::initialization_script()` to inject JavaScript that runs when the webview's page loads: ```rust // src-tauri/src/lib.rs // Load script from an external file (recommended for larger scripts) let script_to_inject = include_str!("../extWebview.js"); WebviewWindowBuilder::new(&app_clone, window_label, url_clone) .title("Remote Session") .inner_size(1024.0, 768.0) .initialization_script(script_to_inject) .build(); ``` ### The Injected Script ```javascript // extWebview.js // This script runs automatically when the webview loads window.addEventListener('DOMContentLoaded', () => { console.log('Initializing injected socket link...'); const ws = new WebSocket('ws://127.0.0.1:8080'); ws.onopen = () => { console.log('Injected socket connected!'); const payload = { command: 'remote_action', message: 'Hello from: ' + window.location.href }; ws.send(JSON.stringify(payload)); }; ws.onmessage = (event) => { console.log('RECEIVED FROM TAURI BACKEND:', JSON.parse(event.data)); // Respond with session data ws.send(JSON.stringify({ localsession: window.localStorage.getItem('token') || 'no-session' })); }; ws.onerror = (err) => { console.error('Injected socket error:', err); }; // Expose socket globally for the page's own code to use window.localAppSocket = ws; }); ``` ### Use Cases - **Bridge external pages**: Allow pages loaded from external URLs to communicate with the Tauri backend via a local WebSocket connection. - **Data extraction**: Inject scripts that read data from external pages (localStorage, DOM elements, cookies) and relay it back to the Tauri backend. - **UI modification**: Modify the appearance or behavior of external pages loaded in a webview. - **Session sharing**: Extract authentication tokens or session data from one webview and relay it to another. ### Important Notes - The injected script runs in the webview's JavaScript context, so it has access to that page's `window`, `document`, and `localStorage`. - For security reasons, injected scripts on external URLs depend on CSP (Content Security Policy). Set `"csp": null` in `tauri.conf.json` to disable CSP restrictions (use with caution). - Use `include_str!()` to load scripts from files at compile time, keeping your Rust code clean. --- ## 12. State Management ### Managed State (Tauri's Built-in) Use `tauri::State` for shared state across commands: ```rust use std::sync::Mutex; use tauri::State; struct AppState { pub user: Mutex>, pub settings: Mutex, } #[tauri::command] fn get_user(state: State<'_, AppState>) -> Option { state.user.lock().unwrap().clone() } #[tauri::command] fn set_user(state: State<'_, AppState>, name: String) { *state.user.lock().unwrap() = Some(name); } pub fn run() { tauri::Builder::default() .manage(AppState { user: Mutex::new(None), settings: Mutex::new(Settings::default()), }) .invoke_handler(tauri::generate_handler![get_user, set_user]) // ... } ``` **Thread-safe types**: Use `Mutex` for simple cases, `RwLock` for read-heavy workloads, or `tokio::sync::Mutex` for async contexts. ### Using the Store Plugin (Persistent KV Storage) For data that persists across app restarts: ```rust // Cargo.toml tauri-plugin-store = "2" ``` ```javascript // Frontend import { Store } from '@tauri-apps/plugin-store'; const store = await Store.load('app-state.json'); await store.set('user-preferences', { theme: 'dark', language: 'en' }); await store.save(); // Read on next launch const prefs = await store.get('user-preferences'); ``` ### Recommended Patterns | Data Type | Approach | |-----------|----------| | Transient UI state (form inputs, selections) | Frontend state management (React state, Vue refs, etc.) | | Shared app state across commands | `tauri::State` with `Mutex`/`RwLock` | | Persistent user preferences | `tauri-plugin-store` | | Structured persistent data (records, models) | `tauri-plugin-sql` with SQLite | | Encrypted sensitive data | `tauri-plugin-stronghold` | --- ## 13. v1 to v2 Migration Reference ### Automated Migration ```bash npm install @tauri-apps/cli@latest npm run tauri migrate ``` This automates config migration and generates capability files from the v1 allowlist. **However**, it is NOT a complete replacement for understanding the changes — manual review is still required. ### Rust API Migration Map | v1 Pattern | v2 Replacement | |------------|----------------| | `tauri::api::dialog::*` | `tauri-plugin-dialog` | | `tauri::api::fs::*` | `std::fs` (Rust) or `tauri-plugin-fs` (JS) | | `tauri::api::http::*` | `tauri-plugin-http` | | `tauri::api::path::*`, `tauri::PathResolver` | `tauri::Manager::path()` | | `tauri::api::process::Command` | `tauri-plugin-shell` | | `tauri::api::shell::*` | `tauri-plugin-shell` | | `tauri::api::clipboard::*` | `tauri-plugin-clipboard-manager` | | `tauri::api::notification::*` | `tauri-plugin-notification` | | `tauri::api::global_shortcut::*` | `tauri-plugin-global-shortcut` | | `tauri::api::updater::*` | `tauri-plugin-updater` | | `tauri::api::cli::*` | `tauri-plugin-cli` | | `tauri::api::os::*` | `tauri-plugin-os` | | `tauri::api::version` | `semver` crate | | `tauri::Window` | `tauri::WebviewWindow` | | `tauri::WindowBuilder` | `tauri::WebviewWindowBuilder` | | `tauri::WindowUrl` | `tauri::WebviewUrl` | | `Manager::get_window()` | `Manager::get_webview_window()` | | `App::window()` | `App::get_webview_window()` | | `tauri::Menu` | `tauri::menu::MenuBuilder` | | `tauri::MenuItem` | `tauri::menu::PredefinedMenuItem` | | `tauri::CustomMenuItem` | `tauri::menu::MenuItemBuilder` | | `tauri::Submenu` | `tauri::menu::SubmenuBuilder` | | `tauri::SystemTray` | `tauri::tray::TrayIconBuilder` | | `App::clipboard_manager()` | `tauri_plugin_clipboard_manager::ClipboardExt` | | `App::global_shortcut_manager()` | `tauri_plugin_global_shortcut::GlobalShortcutExt` | | `Manager::fs_scope()` | `tauri_plugin_fs::FsExt` | | `emit()` | `emit()` (now broadcasts to ALL listeners) | | `emit_to()` | NEW in v2 for targeted emission | | `listen_global()` | Renamed to `listen_any()` | ### JavaScript API Migration Map | v1 Import | v2 Import | |-----------|-----------| | `@tauri-apps/api/tauri` | `@tauri-apps/api/core` | | `@tauri-apps/api/window` | `@tauri-apps/api/webviewWindow` | | `@tauri-apps/api/dialog` | `@tauri-apps/plugin-dialog` | | `@tauri-apps/api/fs` | `@tauri-apps/plugin-fs` | | `@tauri-apps/api/http` | `@tauri-apps/plugin-http` | | `@tauri-apps/api/notification` | `@tauri-apps/plugin-notification` | | `@tauri-apps/api/clipboard` | `@tauri-apps/plugin-clipboard-manager` | | `@tauri-apps/api/shell` | `@tauri-apps/plugin-shell` | | `@tauri-apps/api/global-shortcut` | `@tauri-apps/plugin-global-shortcut` | | `@tauri-apps/api/updater` | `@tauri-apps/plugin-updater` | | `@tauri-apps/api/os` | `@tauri-apps/plugin-os` | | `@tauri-apps/api/process` | `@tauri-apps/plugin-process` | | `@tauri-apps/api/cli` | `@tauri-apps/plugin-cli` | | `tauri-plugin-*` JS packages | `@tauri-apps/plugin-*` | ### Environment Variable Migration | v1 | v2 | |----|----| | `TAURI_PRIVATE_KEY` | `TAURI_SIGNING_PRIVATE_KEY` | | `TAURI_KEY_PASSWORD` | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | | `TAURI_SKIP_DEVSERVER_CHECK` | `TAURI_CLI_NO_DEV_SERVER_WAIT` | | `TAURI_DEV_SERVER_PORT` | `TAURI_CLI_PORT` | | `TAURI_TRAY` | `TAURI_LINUX_AYATANA_APPINDICATOR` | | `TAURI_APPLE_DEVELOPMENT_TEAM` | `APPLE_DEVELOPMENT_TEAM` | | `TAURI_PLATFORM` | `TAURI_ENV_PLATFORM` | | `TAURI_ARCH` | `TAURI_ENV_ARCH` | | `TAURI_DEBUG` | `TAURI_ENV_DEBUG` | ### Windows Origin URL Change **v1**: `https://tauri.localhost` **v2**: `http://tauri.localhost` This means IndexedDB, LocalStorage, and Cookies from v1 apps will **not carry over** to v2. To keep the HTTPS scheme: ```json { "app": { "windows": [{ "useHttpsScheme": true }] } } ``` --- ## 14. Common Anti-Patterns to Avoid ### 1. Using v1 APIs or Import Paths ```rust // WRONG - v1 pattern (tauri::api was REMOVED) use tauri::api::dialog; use tauri::api::fs; // CORRECT - v2 pattern // Use std::fs in Rust, or tauri-plugin-fs on the frontend ``` ```javascript // WRONG - v1 import path import { invoke } from '@tauri-apps/api/tauri'; import { open } from '@tauri-apps/api/dialog'; // CORRECT - v2 import path import { invoke } from '@tauri-apps/api/core'; import { open } from '@tauri-apps/plugin-dialog'; ``` ### 2. Using `tauri.conf.json` v1 Structure ```json // WRONG - v1 structure { "tauri": { "allowlist": { ... }, "windows": [...], "bundle": { ... } }, "build": { "distDir": "../dist", "withGlobalTauri": true } } // CORRECT - v2 structure { "app": { "security": { "capabilities": [...] }, "windows": [...], "withGlobalTauri": true }, "build": { "frontendDist": "../dist" }, "bundle": { ... } } ``` ### 3. Using Old Window Types ```rust // WRONG - v1 types let window = tauri::WindowBuilder::new(&app, "main", tauri::WindowUrl::App("index.html".into())); app.get_window("main"); // CORRECT - v2 types let window = tauri::WebviewWindowBuilder::new(&app, "main", tauri::WebviewUrl::App("index.html".into())); app.get_webview_window("main"); ``` ### 4. Calling `invoke_handler` Multiple Times ```rust // WRONG - only the last one takes effect .invoke_handler(tauri::generate_handler![command_a]) .invoke_handler(tauri::generate_handler![command_b]) // CORRECT - all commands in a single call .invoke_handler(tauri::generate_handler![command_a, command_b]) ``` ### 5. Using Borrowed Types in Async Commands ```rust // WRONG - &str cannot cross async boundary #[tauri::command] async fn process(data: &str) -> String { ... } // CORRECT - use owned types #[tauri::command] async fn process(data: String) -> String { ... } // ALTERNATIVELY - wrap return in Result #[tauri::command] async fn process(data: &str) -> Result { ... } ``` ### 6. Forgetting to Add Permissions for Plugins Every plugin requires permissions to be granted in a capability file. Without them, the plugin's frontend API calls will silently fail or throw permission errors. ```json // WRONG - no permissions for the fs plugin { "permissions": ["core:default"] } // CORRECT - grant fs plugin permissions { "permissions": [ "core:default", "fs:default" ] } ``` ### 7. Creating Windows Outside the Main Thread ```rust // WRONG - window creation from async context without dispatch #[tauri::command] async fn open_window(app_handle: AppHandle) { tauri::WebviewWindowBuilder::new(&app_handle, "new", WebviewUrl::App("page.html".into())) .build(); // This will panic or fail! } // CORRECT - dispatch to main thread #[tauri::command] async fn open_window(app_handle: AppHandle) -> Result<(), String> { app_handle.run_on_main_thread(move || { let _ = tauri::WebviewWindowBuilder::new( &app_handle, "new", tauri::WebviewUrl::App("page.html".into()), ) .build(); }).map_err(|e| e.to_string())?; Ok(()) } ``` ### 8. Not Cleaning Up Event Listeners ```javascript // WRONG - listener leaks memory const component = () => { useEffect(() => { listen('my-event', handler); // Never cleaned up! }, []); }; // CORRECT - clean up on unmount const component = () => { useEffect(() => { const unlisten = listen('my-event', handler); return () => { unlisten.then(fn => fn()); }; }, []); }; ``` --- ## 15. Quick Reference Cheat Sheet ### Tauri Builder Setup ```rust tauri::Builder::default() .plugin(plugin::init()) // Register plugins .setup(|app| Ok(())) // Startup logic .manage(State {}) // Inject state .invoke_handler(tauri::generate_handler![ // Register commands cmd1, cmd2, module::cmd3 ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); ``` ### Command Definition ```rust #[tauri::command] async fn command_name(arg: String) -> Result { ... } ``` ### Frontend Invoke ```javascript import { invoke } from '@tauri-apps/api/core'; const result = await invoke('command_name', { arg: 'value' }); ``` ### Event System ```rust // Rust use tauri::{AppHandle, Emitter}; app_handle.emit("event-name", payload)?; app_handle.emit_to("window-label", "event-name", payload)?; ``` ```javascript // Frontend import { listen, emit, emitTo } from '@tauri-apps/api/event'; const unlisten = await listen('event-name', (event) => { /* event.payload */ }); await emit('event-name', data); await emitTo('window-label', 'event-name', data); ``` ### Window Management ```rust use tauri::{WebviewWindowBuilder, WebviewUrl, Manager}; WebviewWindowBuilder::new(&app, "label", WebviewUrl::App("page.html".into())) .title("Title").inner_size(800.0, 600.0).build()?; app.get_webview_window("label")?.set_focus()?; ``` ### Key v2 Naming Rules - Window type: `WebviewWindow` (not `Window`) - Window builder: `WebviewWindowBuilder` (not `WindowBuilder`) - URL type: `WebviewUrl` (not `WindowUrl`) - Get window: `get_webview_window()` (not `get_window()`) - Core import: `@tauri-apps/api/core` (not `@tauri-apps/api/tauri`) - Plugin imports: `@tauri-apps/plugin-` (not `@tauri-apps/api/`) - Config section: `app` (not `tauri`) - Security model: Capabilities + Permissions (not `allowlist`) ### Official Documentation Links - Main Docs: https://v2.tauri.app - Migration Guide: https://v2.tauri.app/start/migrate/from-tauri-1/ - Capabilities: https://v2.tauri.app/security/capabilities/ - Permissions: https://v2.tauri.app/security/permissions/ - Configuration: https://v2.tauri.app/develop/configuration-files/ - Calling Rust: https://v2.tauri.app/develop/calling-rust/ - Calling Frontend: https://v2.tauri.app/develop/calling-frontend/ - Plugin Index: https://v2.tauri.app/plugin/ - Rust Docs: https://docs.rs/tauri/latest/tauri/ --- **Back to**: [Part 1: Core Architecture, Config, Commands & IPC](./01-core-architecture-config-commands-ipc.md)