tauri-syntax-state
tauri-syntax-state
Quick Reference
State Registration (Tauri 2.x)
| Method | Context | Description |
|---|---|---|
Builder::manage(T) |
Builder chain | Register state during app construction |
App::manage(T) |
setup() hook |
Register state that depends on app initialization |
AppHandle::manage(T) |
Any context | Register state at runtime (rare) |
State Access
| Method | Context | Returns |
|---|---|---|
State<'_, T> |
Command parameter | Injected automatically by Tauri |
app_handle.state::<T>() |
Any code with AppHandle | State<'_, T> (panics if not registered) |
app_handle.try_state::<T>() |
Any code with AppHandle | Option<State<'_, T>> (safe) |
Thread-Safety Wrappers
| Wrapper | Use Case | Lock Method |
|---|---|---|
std::sync::Mutex<T> |
Mutable state, no .await while locked |
.lock().unwrap() |
tokio::sync::Mutex<T> |
Mutable state with .await while locked |
.lock().await |
std::sync::RwLock<T> |
Read-heavy access, concurrent readers | .read().unwrap() / .write().unwrap() |
| None (immutable) | Read-only config data | Direct field access |
Critical Warnings
NEVER use State<'_, T> when you registered Mutex<T> — the type must match EXACTLY or Tauri panics at runtime, not at compile time. If you registered Mutex<Counter>, the command parameter must be State<'_, Mutex<Counter>>.
NEVER wrap managed state in Arc — Tauri already wraps all managed state in Arc internally. Using app.manage(Arc::new(data)) adds a redundant layer.
NEVER lock the same Mutex twice in the same call chain — this causes a deadlock. If a function holding a lock calls another function that also locks, the thread blocks forever.
NEVER use tokio::sync::Mutex unless you need to hold the lock across .await points — std::sync::Mutex is more efficient for synchronous access.
ALWAYS register state before any command tries to access it — accessing unregistered state via State<T> causes a runtime panic.
ALWAYS match the exact registered type in State<'_, T> — including all wrappers like Mutex, RwLock, etc.
Essential Patterns
Pattern 1: Immutable State (Read-Only Config)
// Tauri 2.x — No Mutex needed for read-only data
struct AppConfig {
api_url: String,
max_retries: u32,
}
tauri::Builder::default()
.manage(AppConfig {
api_url: "https://api.example.com".into(),
max_retries: 3,
})
.invoke_handler(tauri::generate_handler![get_api_url])
#[tauri::command]
fn get_api_url(config: tauri::State<'_, AppConfig>) -> String {
config.api_url.clone()
}
Pattern 2: Mutable State with std::sync::Mutex
// Tauri 2.x — Standard pattern for mutable state
use std::sync::Mutex;
#[derive(Default)]
struct Counter {
value: u32,
}
tauri::Builder::default()
.manage(Mutex::new(Counter::default()))
.invoke_handler(tauri::generate_handler![increment, get_count])
#[tauri::command]
fn increment(state: tauri::State<'_, Mutex<Counter>>) -> u32 {
let mut counter = state.lock().unwrap();
counter.value += 1;
counter.value
}
#[tauri::command]
fn get_count(state: tauri::State<'_, Mutex<Counter>>) -> u32 {
state.lock().unwrap().value
}
Pattern 3: Async Commands with tokio::sync::Mutex
// Tauri 2.x — Use tokio Mutex when holding lock across .await
use tokio::sync::Mutex;
struct Database {
connection: String,
}
#[tauri::command]
async fn save_record(
state: tauri::State<'_, Mutex<Database>>,
data: String,
) -> Result<(), String> {
let db = state.lock().await;
// ... perform async database operations while holding lock ...
Ok(())
}
Pattern 4: RwLock for Read-Heavy Access
// Tauri 2.x — Multiple concurrent readers, exclusive writers
use std::sync::RwLock;
struct AppData {
items: Vec<String>,
}
tauri::Builder::default()
.manage(RwLock::new(AppData { items: vec![] }))
#[tauri::command]
fn list_items(state: tauri::State<'_, RwLock<AppData>>) -> Vec<String> {
let data = state.read().unwrap(); // Multiple readers OK
data.items.clone()
}
#[tauri::command]
fn add_item(state: tauri::State<'_, RwLock<AppData>>, item: String) {
let mut data = state.write().unwrap(); // Exclusive access
data.items.push(item);
}
Pattern 5: State Initialization in setup()
// Tauri 2.x — State that depends on app paths or runtime info
tauri::Builder::default()
.setup(|app| {
let db_path = app.path().app_data_dir()?.join("data.db");
app.manage(Database::new(&db_path)?);
Ok(())
})
Pattern 6: Accessing State via AppHandle
// Tauri 2.x — Access state outside of commands
use std::sync::Mutex;
fn background_task(handle: tauri::AppHandle) {
let state = handle.state::<Mutex<Counter>>();
let mut counter = state.lock().unwrap();
counter.value += 1;
}
// Safe variant — returns None if state not registered
fn maybe_access(handle: &tauri::AppHandle) {
if let Some(state) = handle.try_state::<Mutex<Counter>>() {
let counter = state.lock().unwrap();
println!("Count: {}", counter.value);
}
}
Manager Trait
The Manager trait provides state() and try_state(). It is implemented by:
| Type | Description |
|---|---|
App |
Available in setup() |
AppHandle |
Cloneable, Send + Sync, use in threads |
Window |
Window instance |
Webview |
Webview instance |
WebviewWindow |
Combined window + webview |
All of these types can call .state::<T>() and .try_state::<T>().
Multiple State Types
Register multiple state types independently:
tauri::Builder::default()
.manage(AppConfig { /* ... */ })
.manage(Mutex::new(UserSession::default()))
.manage(RwLock::new(DocumentStore::default()))
.invoke_handler(tauri::generate_handler![
get_config,
login,
get_document,
])
#[tauri::command]
fn get_config(config: tauri::State<'_, AppConfig>) -> String {
config.api_url.clone()
}
#[tauri::command]
fn login(session: tauri::State<'_, Mutex<UserSession>>, user: String) {
let mut s = session.lock().unwrap();
s.username = user;
}
Each type is registered and accessed independently. There is no limit on the number of state types.
Reference Links
- references/methods.md — Complete state management API signatures
- references/examples.md — Working code examples for common state patterns
- references/anti-patterns.md — What NOT to do, with WHY explanations