$ cat /posts/tauri-20-state-management-sharing-data-across-app.md
[tags]Tauri 2.0

Tauri 2.0 State Management Sharing Data Across App

drwxr-xr-x2026-01-285 min0 views
Tauri 2.0 State Management Sharing Data Across App

State management in Tauri 2.0 enables sharing data across multiple commands, windows, and application lifecycle providing centralized data storage accessible from anywhere in your applicationโ€”essential for maintaining user sessions, application settings, cached data, and coordinating behavior between components while ensuring thread-safe access preventing data races in concurrent operations. Tauri's state management uses Rust's State type injected into commands automatically providing access to globally managed data wrapped in Mutex or RwLock for thread safety, supporting complex state structures including database connections, configuration objects, caches, and application-wide counters maintaining consistency across the entire application. This comprehensive guide covers understanding Tauri's state management architecture and dependency injection, creating simple state with primitive types and basic structures, implementing thread-safe state using Mutex and RwLock preventing data races, managing complex state structures including nested data and collections, accessing state from commands with automatic injection, sharing state across multiple windows coordinating UI updates, implementing state persistence saving and loading from disk, handling state errors and recovery strategies, optimizing state performance with read-write locks, and building real-world state patterns including user sessions, settings management, and caching systems. Mastering state patterns enables building sophisticated applications maintaining consistent data, coordinating multi-window behavior, caching expensive computations, managing user authentication, persisting configuration across restarts, and implementing undo/redo functionality. Before proceeding, understand IPC communication and command creation.

State Management Fundamentals

Tauri state is managed through the manage() method during app initialization making data globally accessible to all commands through dependency injection maintaining clean architecture without global variables.

rustbasic_state.rs
// src-tauri/src/main.rs
use std::sync::Mutex;
use tauri::State;

// Define simple state struct
struct AppState {
    counter: Mutex<i32>,
}

// Commands accessing state
#[tauri::command]
fn increment(state: State<AppState>) -> i32 {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    *counter
}

#[tauri::command]
fn decrement(state: State<AppState>) -> i32 {
    let mut counter = state.counter.lock().unwrap();
    *counter -= 1;
    *counter
}

#[tauri::command]
fn get_counter(state: State<AppState>) -> i32 {
    *state.counter.lock().unwrap()
}

#[tauri::command]
fn reset(state: State<AppState>) {
    let mut counter = state.counter.lock().unwrap();
    *counter = 0;
}

fn main() {
    // Initialize state
    let app_state = AppState {
        counter: Mutex::new(0),
    };

    tauri::Builder::default()
        .manage(app_state) // Register state
        .invoke_handler(tauri::generate_handler![
            increment,
            decrement,
            get_counter,
            reset,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

// Frontend: Using state
import { invoke } from "@tauri-apps/api/core";
import { useState, useEffect } from "react";

function CounterApp() {
  const [count, setCount] = useState(0);

  const loadCounter = async () => {
    const value = await invoke<number>("get_counter");
    setCount(value);
  };

  const increment = async () => {
    const value = await invoke<number>("increment");
    setCount(value);
  };

  const decrement = async () => {
    const value = await invoke<number>("decrement");
    setCount(value);
  };

  const reset = async () => {
    await invoke("reset");
    await loadCounter();
  };

  useEffect(() => {
    loadCounter();
  }, []);

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Complex State Structures

Real applications require complex state including user data, settings, caches, and configuration using nested structures and collections maintaining organized data accessible from multiple commands.

rustcomplex_state.rs
use std::sync::Mutex;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use tauri::State;

// User session state
#[derive(Clone, Serialize, Deserialize)]
struct User {
    id: u32,
    username: String,
    email: String,
    roles: Vec<String>,
}

// Application settings
#[derive(Clone, Serialize, Deserialize)]
struct Settings {
    theme: String,
    language: String,
    font_size: u32,
    notifications_enabled: bool,
}

impl Default for Settings {
    fn default() -> Self {
        Settings {
            theme: "dark".to_string(),
            language: "en".to_string(),
            font_size: 14,
            notifications_enabled: true,
        }
    }
}

// Cache for expensive computations
type Cache = HashMap<String, String>;

// Main application state
struct AppState {
    user: Mutex<Option<User>>,
    settings: Mutex<Settings>,
    cache: Mutex<Cache>,
    session_data: Mutex<HashMap<String, serde_json::Value>>,
}

// User management commands
#[tauri::command]
fn login(state: State<AppState>, username: String, email: String) -> Result<User, String> {
    let user = User {
        id: 1,
        username: username.clone(),
        email,
        roles: vec!["user".to_string()],
    };

    let mut current_user = state.user.lock().unwrap();
    *current_user = Some(user.clone());

    Ok(user)
}

#[tauri::command]
fn logout(state: State<AppState>) {
    let mut user = state.user.lock().unwrap();
    *user = None;
}

#[tauri::command]
fn get_current_user(state: State<AppState>) -> Option<User> {
    state.user.lock().unwrap().clone()
}

#[tauri::command]
fn is_authenticated(state: State<AppState>) -> bool {
    state.user.lock().unwrap().is_some()
}

// Settings management commands
#[tauri::command]
fn get_settings(state: State<AppState>) -> Settings {
    state.settings.lock().unwrap().clone()
}

#[tauri::command]
fn update_settings(state: State<AppState>, new_settings: Settings) {
    let mut settings = state.settings.lock().unwrap();
    *settings = new_settings;
}

#[tauri::command]
fn update_theme(state: State<AppState>, theme: String) {
    let mut settings = state.settings.lock().unwrap();
    settings.theme = theme;
}

#[tauri::command]
fn update_font_size(state: State<AppState>, size: u32) {
    let mut settings = state.settings.lock().unwrap();
    settings.font_size = size;
}

// Cache management commands
#[tauri::command]
fn get_cached(state: State<AppState>, key: String) -> Option<String> {
    let cache = state.cache.lock().unwrap();
    cache.get(&key).cloned()
}

#[tauri::command]
fn set_cached(state: State<AppState>, key: String, value: String) {
    let mut cache = state.cache.lock().unwrap();
    cache.insert(key, value);
}

#[tauri::command]
fn clear_cache(state: State<AppState>) {
    let mut cache = state.cache.lock().unwrap();
    cache.clear();
}

// Session data management
#[tauri::command]
fn set_session_data(
    state: State<AppState>,
    key: String,
    value: serde_json::Value
) {
    let mut session = state.session_data.lock().unwrap();
    session.insert(key, value);
}

#[tauri::command]
fn get_session_data(
    state: State<AppState>,
    key: String
) -> Option<serde_json::Value> {
    let session = state.session_data.lock().unwrap();
    session.get(&key).cloned()
}

// Initialize state
fn main() {
    let app_state = AppState {
        user: Mutex::new(None),
        settings: Mutex::new(Settings::default()),
        cache: Mutex::new(HashMap::new()),
        session_data: Mutex::new(HashMap::new()),
    };

    tauri::Builder::default()
        .manage(app_state)
        .invoke_handler(tauri::generate_handler![
            login,
            logout,
            get_current_user,
            is_authenticated,
            get_settings,
            update_settings,
            update_theme,
            update_font_size,
            get_cached,
            set_cached,
            clear_cache,
            set_session_data,
            get_session_data,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Read-Write Locks for Performance

For read-heavy workloads, RwLock allows multiple concurrent readers improving performance over Mutex which serializes all access maintaining thread safety with better throughput.

rustrwlock_state.rs
use std::sync::RwLock;
use std::collections::HashMap;
use tauri::State;

// State with RwLock for read-heavy access
struct AppState {
    config: RwLock<HashMap<String, String>>,
    statistics: RwLock<AppStats>,
}

#[derive(Clone)]
struct AppStats {
    requests_count: u64,
    errors_count: u64,
    last_request_time: Option<String>,
}

// Read operations (multiple readers allowed)
#[tauri::command]
fn get_config_value(state: State<AppState>, key: String) -> Option<String> {
    let config = state.config.read().unwrap();
    config.get(&key).cloned()
}

#[tauri::command]
fn get_all_config(state: State<AppState>) -> HashMap<String, String> {
    let config = state.config.read().unwrap();
    config.clone()
}

#[tauri::command]
fn get_statistics(state: State<AppState>) -> (u64, u64) {
    let stats = state.statistics.read().unwrap();
    (stats.requests_count, stats.errors_count)
}

// Write operations (exclusive access)
#[tauri::command]
fn set_config_value(state: State<AppState>, key: String, value: String) {
    let mut config = state.config.write().unwrap();
    config.insert(key, value);
}

#[tauri::command]
fn increment_request_count(state: State<AppState>) {
    let mut stats = state.statistics.write().unwrap();
    stats.requests_count += 1;
    stats.last_request_time = Some(chrono::Utc::now().to_rfc3339());
}

#[tauri::command]
fn increment_error_count(state: State<AppState>) {
    let mut stats = state.statistics.write().unwrap();
    stats.errors_count += 1;
}

// When to use Mutex vs RwLock:
// - Mutex: Write-heavy operations, simple access patterns
// - RwLock: Read-heavy operations, multiple concurrent readers

// Example: Read-heavy configuration
struct ConfigState {
    data: RwLock<HashMap<String, serde_json::Value>>,
}

#[tauri::command]
fn get_config(state: State<ConfigState>, key: String) -> Option<serde_json::Value> {
    // Multiple threads can read simultaneously
    let data = state.data.read().unwrap();
    data.get(&key).cloned()
}

#[tauri::command]
fn update_config(state: State<ConfigState>, key: String, value: serde_json::Value) {
    // Exclusive write access
    let mut data = state.data.write().unwrap();
    data.insert(key, value);
}

Persisting State to Disk

Save state to disk ensuring data persists across application restarts loading saved state on startup maintaining user preferences and application data between sessions.

ruststate_persistence.rs
use std::sync::Mutex;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager, State};

#[derive(Clone, Serialize, Deserialize)]
struct PersistentSettings {
    theme: String,
    language: String,
    window_size: (u32, u32),
    recent_files: Vec<String>,
}

impl Default for PersistentSettings {
    fn default() -> Self {
        PersistentSettings {
            theme: "dark".to_string(),
            language: "en".to_string(),
            window_size: (800, 600),
            recent_files: Vec::new(),
        }
    }
}

struct AppState {
    settings: Mutex<PersistentSettings>,
    data_dir: PathBuf,
}

// Get settings file path
fn get_settings_path(app: &AppHandle) -> Result<PathBuf, String> {
    let app_dir = app
        .path_resolver()
        .app_data_dir()
        .ok_or("Failed to get app data directory")?;
    
    std::fs::create_dir_all(&app_dir)
        .map_err(|e| format!("Failed to create data directory: {}", e))?;
    
    Ok(app_dir.join("settings.json"))
}

// Load settings from disk
fn load_settings(app: &AppHandle) -> PersistentSettings {
    match get_settings_path(app) {
        Ok(path) => {
            if path.exists() {
                match std::fs::read_to_string(&path) {
                    Ok(contents) => {
                        match serde_json::from_str(&contents) {
                            Ok(settings) => return settings,
                            Err(e) => eprintln!("Failed to parse settings: {}", e),
                        }
                    }
                    Err(e) => eprintln!("Failed to read settings: {}", e),
                }
            }
        }
        Err(e) => eprintln!("Failed to get settings path: {}", e),
    }
    
    PersistentSettings::default()
}

// Save settings to disk
fn save_settings(app: &AppHandle, settings: &PersistentSettings) -> Result<(), String> {
    let path = get_settings_path(app)?;
    let contents = serde_json::to_string_pretty(settings)
        .map_err(|e| format!("Failed to serialize settings: {}", e))?;
    
    std::fs::write(&path, contents)
        .map_err(|e| format!("Failed to write settings: {}", e))?;
    
    Ok(())
}

// Commands
#[tauri::command]
fn get_settings(state: State<AppState>) -> PersistentSettings {
    state.settings.lock().unwrap().clone()
}

#[tauri::command]
fn update_settings(
    state: State<AppState>,
    app: AppHandle,
    settings: PersistentSettings
) -> Result<(), String> {
    // Update in-memory state
    {
        let mut current = state.settings.lock().unwrap();
        *current = settings.clone();
    }
    
    // Persist to disk
    save_settings(&app, &settings)?;
    
    Ok(())
}

#[tauri::command]
fn add_recent_file(
    state: State<AppState>,
    app: AppHandle,
    file_path: String
) -> Result<(), String> {
    let mut settings = state.settings.lock().unwrap();
    
    // Remove if already exists
    settings.recent_files.retain(|f| f != &file_path);
    
    // Add to front
    settings.recent_files.insert(0, file_path);
    
    // Limit to 10 recent files
    settings.recent_files.truncate(10);
    
    // Save to disk
    save_settings(&app, &settings)?;
    
    Ok(())
}

// Setup function to load state on startup
fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let handle = app.handle();
            
            // Load settings from disk
            let settings = load_settings(&handle);
            
            // Get data directory
            let data_dir = handle
                .path_resolver()
                .app_data_dir()
                .expect("Failed to get data directory");
            
            // Initialize state
            let app_state = AppState {
                settings: Mutex::new(settings),
                data_dir,
            };
            
            app.manage(app_state);
            
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            get_settings,
            update_settings,
            add_recent_file,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Sharing State Across Windows

State is automatically shared across all windows enabling coordination and synchronization with events notifying windows of state changes maintaining consistency. Learn more about window management.

rustmulti_window_state.rs
use tauri::{AppHandle, Manager, State, Window};
use std::sync::Mutex;

struct SharedState {
    data: Mutex<Vec<String>>,
}

// Update state and notify all windows
#[tauri::command]
fn add_item(state: State<SharedState>, app: AppHandle, item: String) {
    // Update state
    {
        let mut data = state.data.lock().unwrap();
        data.push(item.clone());
    }
    
    // Notify all windows
    app.emit_all("state-updated", item).ok();
}

#[tauri::command]
fn remove_item(state: State<SharedState>, app: AppHandle, index: usize) {
    // Update state
    {
        let mut data = state.data.lock().unwrap();
        if index < data.len() {
            data.remove(index);
        }
    }
    
    // Notify all windows
    app.emit_all("state-updated", ()).ok();
}

#[tauri::command]
fn get_items(state: State<SharedState>) -> Vec<String> {
    state.data.lock().unwrap().clone()
}

// Frontend: React component syncing with state
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";

function SharedDataList() {
  const [items, setItems] = useState<string[]>([]);
  const [newItem, setNewItem] = useState("");

  const loadItems = async () => {
    const data = await invoke<string[]>("get_items");
    setItems(data);
  };

  useEffect(() => {
    // Load initial data
    loadItems();

    // Listen for updates from other windows
    const unlisten = listen("state-updated", () => {
      loadItems();
    });

    return () => {
      unlisten.then(fn => fn());
    };
  }, []);

  const addItem = async () => {
    if (newItem.trim()) {
      await invoke("add_item", { item: newItem });
      setNewItem("");
    }
  };

  const removeItem = async (index: number) => {
    await invoke("remove_item", { index });
  };

  return (
    <div>
      <h2>Shared Data (Synced Across Windows)</h2>
      <ul>
        {items.map((item, index) => (
          <li key={index}>
            {item}
            <button onClick={() => removeItem(index)}>Remove</button>
          </li>
        ))}
      </ul>
      <input
        value={newItem}
        onChange={(e) => setNewItem(e.target.value)}
        placeholder="New item"
      />
      <button onClick={addItem}>Add</button>
    </div>
  );
}

State Management Best Practices

  • Use RwLock for Reads: Prefer RwLock over Mutex for read-heavy workloads enabling concurrent readers
  • Minimize Lock Duration: Hold locks for shortest time possible preventing contention and deadlocks
  • Clone When Necessary: Clone data before releasing lock avoiding lifetime issues with borrowed data
  • Persist Important State: Save critical state to disk ensuring data survives restarts
  • Notify on Changes: Emit events when state changes keeping all windows synchronized
  • Handle Poison Errors: Use unwrap_or_else instead of unwrap for production code
  • Separate Concerns: Organize state into logical groups (user, settings, cache) maintaining clarity
  • Use Default Trait: Implement Default for state structs simplifying initialization
  • Document State: Add doc comments explaining state purpose and access patterns
  • Test State Logic: Write unit tests for state operations ensuring thread safety
Thread Safety: Always use Mutex or RwLock for shared stateโ€”Rust won't compile without proper synchronization! The borrow checker prevents data races at compile time, but you must choose appropriate synchronization primitives for your access patterns.

Real-World Example: User Session

rustsession_management.rs
// Complete user session management
use std::sync::RwLock;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager, State};

#[derive(Clone, Serialize, Deserialize)]
struct User {
    id: u32,
    username: String,
    email: String,
    token: String,
    expires_at: i64,
}

#[derive(Clone, Serialize, Deserialize)]
struct UserPreferences {
    theme: String,
    language: String,
    notifications: bool,
}

struct SessionState {
    user: RwLock<Option<User>>,
    preferences: RwLock<UserPreferences>,
    last_activity: RwLock<Option<i64>>,
}

#[tauri::command]
async fn login(
    state: State<'_, SessionState>,
    app: AppHandle,
    username: String,
    password: String
) -> Result<User, String> {
    // Simulate authentication
    let user = User {
        id: 1,
        username: username.clone(),
        email: format!("{}@example.com", username),
        token: uuid::Uuid::new_v4().to_string(),
        expires_at: chrono::Utc::now().timestamp() + 3600,
    };
    
    // Update state
    {
        let mut current_user = state.user.write().unwrap();
        *current_user = Some(user.clone());
    }
    
    {
        let mut activity = state.last_activity.write().unwrap();
        *activity = Some(chrono::Utc::now().timestamp());
    }
    
    // Load user preferences
    load_user_preferences(&app, &state, user.id).await?;
    
    // Notify all windows
    app.emit_all("user-logged-in", &user).ok();
    
    Ok(user)
}

#[tauri::command]
fn logout(state: State<SessionState>, app: AppHandle) {
    {
        let mut user = state.user.write().unwrap();
        *user = None;
    }
    
    {
        let mut activity = state.last_activity.write().unwrap();
        *activity = None;
    }
    
    app.emit_all("user-logged-out", ()).ok();
}

#[tauri::command]
fn get_current_user(state: State<SessionState>) -> Option<User> {
    state.user.read().unwrap().clone()
}

#[tauri::command]
fn is_session_valid(state: State<SessionState>) -> bool {
    let user = state.user.read().unwrap();
    
    if let Some(user) = user.as_ref() {
        let now = chrono::Utc::now().timestamp();
        return user.expires_at > now;
    }
    
    false
}

#[tauri::command]
fn update_activity(state: State<SessionState>) {
    let mut activity = state.last_activity.write().unwrap();
    *activity = Some(chrono::Utc::now().timestamp());
}

#[tauri::command]
fn get_user_preferences(state: State<SessionState>) -> UserPreferences {
    state.preferences.read().unwrap().clone()
}

#[tauri::command]
async fn update_user_preferences(
    state: State<'_, SessionState>,
    app: AppHandle,
    prefs: UserPreferences
) -> Result<(), String> {
    {
        let mut preferences = state.preferences.write().unwrap();
        *preferences = prefs.clone();
    }
    
    // Save to disk
    save_user_preferences(&app, &prefs).await?;
    
    // Notify windows
    app.emit_all("preferences-updated", prefs).ok();
    
    Ok(())
}

async fn load_user_preferences(
    app: &AppHandle,
    state: &State<'_, SessionState>,
    user_id: u32
) -> Result<(), String> {
    // Load from disk (simulated)
    let prefs = UserPreferences {
        theme: "dark".to_string(),
        language: "en".to_string(),
        notifications: true,
    };
    
    let mut preferences = state.preferences.write().unwrap();
    *preferences = prefs;
    
    Ok(())
}

async fn save_user_preferences(
    app: &AppHandle,
    prefs: &UserPreferences
) -> Result<(), String> {
    // Save to disk (implementation)
    Ok(())
}

Next Steps

Conclusion

Mastering state management in Tauri 2.0 enables building sophisticated desktop applications maintaining consistent data across commands, windows, and application lifecycle with thread-safe access preventing data races in concurrent operations while supporting complex structures including user sessions, application settings, caches, and coordination data. Tauri's state system leverages Rust's type safety and concurrency primitives providing Mutex for exclusive access and RwLock for read-heavy workloads with automatic dependency injection through command parameters maintaining clean architecture without global variables or unsafe code. Understanding state patterns including proper lock usage minimizing contention, state persistence saving critical data, multi-window coordination through events, error handling with poison recovery, and performance optimization with appropriate synchronization establishes foundation for building production-ready applications managing complex workflows, user data, and application configuration maintaining reliability throughout operation. Your Tauri applications now possess robust state management enabling features like user authentication, settings persistence, data caching, multi-window coordination, and undo/redo functionality delivering professional desktop experiences!

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.