Skip to content

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.

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.

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).

src-tauri/src/lib.rs
use std::sync::Mutex;
// We wrap the integer in a Mutex to allow safe concurrent access
struct AppState {
counter: Mutex<i32>,
}

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.

src-tauri/src/lib.rs
#[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");
}

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.

src-tauri/src/lib.rs
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
}

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/TypeScript
import { 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.