Rust Backend: State Management
Rust Backend: State Management
Section titled “Rust Backend: State Management”In the previous chapters, we created Rust commands that performed simple tasks: receiving input, processing it, and returning a result. These commands were stateless—they didn’t remember anything from one execution to the next. However, most real-world applications need to remember things. Whether it’s a user’s login session, a database connection, or a simple counter, you need a way to maintain state.
The Challenge of Stateless Commands
Section titled “The Challenge of Stateless Commands”Imagine your Tauri command is a waiter at a restaurant. In a stateless model, every time the waiter comes to your table, they have completely forgotten who you are and what you ordered previously. To have a coherent meal, the waiter needs a notepad to write things down and reference them later. In Tauri, that notepad is tauri::State.
Rust functions are naturally isolated. To share data between the main process (where the app starts) and your command handlers (which run when the frontend asks), Tauri provides a managed state container system.
Defining the State Struct
Section titled “Defining the State Struct”First, we need to define what our state looks like. In Rust, we use a struct. For this tutorial, we will build a simple counter that persists even if the frontend reloads.
Because Tauri commands are asynchronous and can be called from multiple threads simultaneously, we cannot simply use a mutable variable. We need to wrap our data in a thread-safe locking mechanism. The most common standard approach is Mutex (Mutual Exclusion).
use std::sync::Mutex;
// We wrap the integer in a Mutex to allow safe concurrent accessstruct AppState { counter: Mutex<i32>,}Initializing and Managing State
Section titled “Initializing and Managing State”Once the struct is defined, we must tell Tauri to “manage” it. This happens during the application setup in the Builder. When you call .manage(), Tauri injects this instance into its internal state registry, making it accessible to any command.
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() // Initialize the state with a counter starting at 0 .manage(AppState { counter: Mutex::new(0) }) .invoke_handler(tauri::generate_handler![increment_counter]) .run(tauri::generate_context!()) .expect("error while running tauri application");}Accessing State in Commands
Section titled “Accessing State in Commands”Now that Tauri is holding our state, how do we get it back? We use the tauri::State guard as an argument in our command function. Tauri’s dependency injection system automatically detects this argument and passes the correct state instance.
To modify the data, we must first lock the Mutex. This ensures that only one command can modify the counter at a time, preventing race conditions.
use tauri::State;
#[tauri::command]fn increment_counter(state: State<AppState>) -> i32 { // Lock the mutex to get mutable access to the inner integer // .unwrap() is used here to handle the rare case of a "poisoned" mutex let mut count = state.counter.lock().unwrap();
// Modify the value *count += 1;
// Return the new value to the frontend *count}Putting It All Together
Section titled “Putting It All Together”On the frontend, this looks exactly like any other command invocation. The JavaScript does not need to know about Mutex or State. It simply asks the backend to increment and displays the result.
// Frontend JavaScript/TypeScriptimport { invoke } from '@tauri-apps/api/core';
async function addToCount() { const newCount = await invoke('increment_counter'); console.log("Current count from Rust:", newCount);}With this setup, you can reload the web page (Ctrl+R or Cmd+R), and the counter will not reset to zero. The state lives in the Rust backend process, which stays alive independently of the webview.