Released · improving
Tauri guide · 4/6
State in a Tauri app can live in two places. Values that only the UI cares about belong in your frontend framework's state management. Values shared by several windows, or tied to files, databases and the network, are safer in the Rust core. This chapter covers managed state in Rust, the data format between the two sides, and plugins that persist data to disk.
Register Rust-side state with the builder's manage method and receive it in commands through a State<'_, T> parameter. Commands may run concurrently on several threads, so wrap anything mutable in a Mutex.
use std::sync::Mutex;
use serde::Serialize;
use tauri::State;
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct Todo {
id: u32,
title: String,
is_done: bool,
}
#[derive(Default)]
struct AppState {
todos: Vec<Todo>,
next_id: u32,
}
#[tauri::command]
fn add_todo(state: State<'_, Mutex<AppState>>, title: String) -> Vec<Todo> {
let mut app = state.lock().unwrap();
let id = app.next_id;
app.next_id += 1;
app.todos.push(Todo { id, title, is_done: false });
app.todos.clone()
}
#[tauri::command]
fn list_todos(state: State<'_, Mutex<AppState>>) -> Vec<Todo> {
state.lock().unwrap().todos.clone()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(Mutex::new(AppState::default()))
.invoke_handler(tauri::generate_handler![add_todo, list_todos])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}The type in State must match the managed type exactly: registering Mutex<AppState> and asking for State<'_, AppState> panics at runtime. Outside commands, for example in the setup hook or an event handler, import the Manager trait and call app.state::<Mutex<AppState>>(). If a lock has to be held across an .await, use Tokio's async Mutex instead of the standard one.
Command arguments and return values pass through serde as JSON. Thanks to #[serde(rename_all = "camelCase")], the Rust field is_done appears as isDone in JavaScript. Declare a matching type in the frontend and pass it to invoke as a type argument.
import { invoke } from "@tauri-apps/api/core";
type Todo = { id: number; title: string; isDone: boolean };
const todos = await invoke<Todo[]>("add_todo", { title: "Read the docs" });
console.log(todos.filter((todo) => !todo.isDone).length);Errors must be serializable too. A common pattern is to define your own error type with thiserror and implement Serialize by hand, which lets you use the ? operator inside commands.
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
}
impl serde::Serialize for Error {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
#[tauri::command]
fn read_note(path: String) -> Result<String, Error> {
Ok(std::fs::read_to_string(path)?)
}For small key-value data such as preferences or a recent-files list, use the store plugin. Add it with npm run tauri add store and make sure store:default is in your capability. Store files are written to the app data directory.
import { load } from "@tauri-apps/plugin-store";
const store = await load("settings.json", { autoSave: false });
await store.set("theme", "dark");
const theme = await store.get<string>("theme");
await store.save();On the Rust side, import tauri_plugin_store::StoreExt and open the same store with app.store("settings.json")?, for instance to read settings at startup before creating windows.
To read and write files directly, use the fs plugin. Prefer paths relative to a BaseDirectory over absolute paths; Tauri resolves app data and config folders correctly on every OS.
import { readTextFile, writeTextFile, BaseDirectory } from "@tauri-apps/plugin-fs";
await writeTextFile("notes.txt", "First note", { baseDir: BaseDirectory.AppData });
const text = await readTextFile("notes.txt", { baseDir: BaseDirectory.AppData });fs:default only grants read access to the app's own directories. For writes, add a predefined permission such as fs:allow-appdata-write, or attach a path scope to a specific command permission so only the needed locations are open. In Rust, app.path().app_data_dir() returns the same folder for use with the standard library.
For relational data, the sql plugin exposes SQLite (and, depending on features, MySQL or PostgreSQL) to the frontend. Alternatively, use a crate such as sqlx or rusqlite in Rust and expose only results through commands. Apps with many business rules often prefer the latter, because validation and tests stay in one place.
manage, receive it with State<'_, T> and wrap mutable values in a Mutex.rename_all = "camelCase" keeps names aligned on both sides.BaseDirectory for files.
0 comments
Sign in · Sign in to leave a comment.
Be the first to comment.