Skip to main content

Managing State in Desktop Applications with Rust and Tauri

· 7 min read
Ronald Pereira

Currently, we are getting to know Tauri and Rust for Krater development. A few days ago, I found in Tauri's documentation that its applications have a State Manager that allows sharing any type of state globally within the application. In this article, we will review in detail how to take advantage of this when building applications with Tauri! 🚀

I built the following application as a demonstration for the article. You can find the complete code here.

demo.gif

The present material was made with the following technologies and versions:

TechVersions
Tauri1.2
Rust1.68.2
React18.0+

Defining State

Let's open the file main.rs which by default has this structure:

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

// Learn more about Tauri commands at <https://tauri.app/v1/guides/features/command>
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

We proceed to define the structure of the counter state. We can define it before the main() function:

//...

use std::sync::Mutex;

struct Counter {
count: Mutex<i32>,
}

//...

Why use a Mutex?

Mutex is a practical way to share data concurrently between threads. Its main feature is that it is capable of limiting access to data from other threads, ensuring the integrity and security of the shared data.

Suppose A and B are two threads, and the number 5 is stored in shared memory. While both A and B can access this memory location, they cannot do so simultaneously or update it concurrently without generating memory problems. To prevent such issues, the Mutex mechanism is used to block access while the memory is being used or written to. In this case, we will use the lock() function to block other threads from accessing the count data.

By using Mutex with count, we guarantee that the Tauri State Manager can share and persist the counter state between threads and globally. If count does not use Mutex, the state of count is going to be always zero and also it wont be possible to update its value.

Tip: Mutex is short for "mutual exclusion."

Why i32?

Our counter is a numerical value, and we want the number to have the range of negative and positive numbers.

Add State to the State Manager

In the main() function, we initialize the counter state using the manage() function:

// ...

fn main() {
tauri::Builder::default()
.manage(Counter { count: Mutex::new(0) })
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

The manage() function is a public function that resides within the implementation of the Tauri Builder struct, and we use it to add the counter state. It is possible to call this method any number of times, as long as we use a different type (T).

Using states in commands

A practical way to use the app states is on Tauri commands. Let's explore this by creating a new command calculate():

use tauri::State;

#[tauri::command]
fn calculate(method: String, state: State<Counter>) -> i32 {
// ...
}

To get the counter state, we just need to add a state parameter in the function, this parameter is a tauri::State type of Counter type, defining the type of the state is important because it allows Tauri to recognize which state is required in this command.

What is the logic?

To update the counter, we simply add or subtract by 1 if the method parameter (a &str) matches the literal strings "add" or "subtract". If method does not match any of those options, we do not perform any operations. Finally, we return the updated value of the counter.

#[tauri::command]
fn calculate(method: &str, state: State<Counter>) -> i32 {
let mut counter = state.count.lock().unwrap();

match method {
"add" => {
*counter = *counter + 1;
},
"subtract" => {
*counter = *counter - 1;
},
_ => ()
}

*counter
}

Let's review the function:

  1. We declare a mutable variable counter to which we assign the value of the mutex lock of the count field of the state variable. This means that we are blocking other threads from accessing the count data. We’re going to use the mutable reference to update the counter, and at the end of the scope the lock is released (this is the default behavior).
  2. We use a match to compare with the literal strings add and subtract. If it does not match any of the cases, we do not perform any action.
  3. The function returns the value of counter.
  4. The mutex lock is released when the variable counter goes out of the function scope, allowing another thread to acquire it.

Why use *counter?

The reason we use *counter when updating its value as well as in the function's return is that we are dealing with a mutable variable that is wrapped in a mutex lock.

With *, we will be dereferencing the value of the variable to be able to modify it. This is necessary because the mutex lock returns a guard object that maintains a reference to the value of the variable.

With this, we can already use the calculate(...) command from the frontend. In this demo, I used React to make the UI:

import { useState } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import Button from "./components/Button";

export default function App() {
const [counter, setCounter] = useState < number > 0;

async function add() {
setCounter(await invoke("calculate", { method: "add" }));
}

async function subtract() {
setCounter(await invoke("calculate", { method: "subtract" }));
}

return (
<div className="flex flex-col justify-center items-center h-screen space-y-8">
<div className="text-6xl text-gray-800 font-bold">
<span>{counter}</span>
</div>
<div className="flex justify-between space-x-4">
<Button
className="bg-red-700 text-white hover:bg-red-800 active:bg-red-900 text-4xl font-extrabold"
onClick={() => subtract()}
>
-
</Button>
<Button
className="bg-indigo-700 text-white hover:bg-indigo-800 active:bg-indigo-900 text-4xl font-extrabold"
onClick={() => add()}
>
+
</Button>
</div>
</div>
);
}

Extra: State Beyond Commands

Working on Krater, we have had the need to share a Pool Connection as state between threads to perform operations with the database. When doing this, we realized that it is possible to obtain and define any state if we have a reference to the application's AppHandler:

//...
use tauri::{Manager, State};

struct Name(String);

//...

fn main() {
tauri::Builder::default()
.setup(|app: &mut App| {
app.manage(Name("Counter App".into()));

let app_name: State<Name> = app.state();

let counter_state: State<Counter> = app.state();

let counter = counter_state.count.lock().unwrap();

assert_eq!(app_name.0, "Counter App");
assert_eq!(*counter, 0);

Ok(())
})
.manage(Counter {
count: Mutex::new(0),
})
.invoke_handler(tauri::generate_handler![calculate])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

This is possible because app is a mutable reference to the instance of App. If we check the Tauri documentation, we see that the App type implements the Manager trait, which allows us to manipulate global items of the application and use methods such as state(), emit(), manage(), among others.

In the last code modification, we can notice that in the setup() function, through the mutable reference of app, we can use the manage() function to store new states, and we can bring states from the State Manager using the state() function.

You can read more about Tauri::Manager and other features in the official Tauri documentation.

With this, we have achieved the goal of the article. I hope this article has been enjoyable and useful for you. 🚀 You can access the complete code at the following link.

Discover the secrets behind our successful software with our book "MoonGuard: The Software Creator’s Journey" to learn how to create successful Laravel package from scratch! In addition to our website, we also maintain an active presence on Twitter (@moonguard_dev). By following us on Twitter, you'll be the first to know about any breaking news or important announcements regarding MoonGuard. So be sure to check us out and stay connected!