All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.
Rust's ownership system is the right idea. It's also the biggest learning curve. After 7 shipped apps, here are the patterns I actually use daily.
Clone When It's Cheap, Reference When It Matters
The beginner mistake: fighting the borrow checker by cloning everything.
The intermediate mistake: avoiding clones so aggressively that the code becomes unreadable.
The practical rule: clone String, PathBuf, and small structs freely. Don't clone large data structures or anything in a hot loop.
// Fine — String clone is cheap and avoids lifetime complexity
let path = file.path.clone();
tokio::spawn(async move {
process(path).await
});
// Don't clone — reference the slice directly
fn process_lines(lines: &[&str]) { ... }
Arc for Shared Ownership Across Threads
When multiple threads need the same data:
use std::sync::Arc;
let config = Arc::new(AppConfig::load()?);
let config_clone = Arc::clone(&config);
tokio::spawn(async move {
use_config(&config_clone).await;
});
// Original config still valid here
use_config(&config).await;
Arc is reference-counted shared ownership. Clone the Arc, not the data. Cheap.
Mutex for Mutable Shared State
use std::sync::{Arc, Mutex};
let state = Arc::new(Mutex::new(AppState::new()));
// In Tauri command
#[tauri::command]
fn update_state(
state: tauri::State<Arc<Mutex<AppState>>>,
value: String
) {
let mut s = state.lock().unwrap();
s.value = value;
}
Lock, use, drop. Keep the lock scope small. Don't hold a std::sync::Mutex lock across .await points — use tokio::sync::Mutex if you need that.
The Builder Pattern for Complex Structs
When a struct has many optional fields, constructors with a dozen parameters get ugly fast.
pub struct TransferConfig {
pub concurrency: usize,
pub chunk_size: usize,
pub verify_hash: bool,
pub retry_count: u32,
}
impl TransferConfig {
pub fn default() -> Self {
Self {
concurrency: 6,
chunk_size: 65536,
verify_hash: true,
retry_count: 3,
}
}
pub fn with_concurrency(mut self, n: usize) -> Self {
self.concurrency = n;
self
}
}
// Usage
let config = TransferConfig::default()
.with_concurrency(4);
Ergonomic construction without a dozen function parameters.
Type States for Compile-Time Correctness
Instead of runtime checks ("is this device connected?"), encode state in the type system. The compiler enforces valid state transitions.
struct Disconnected;
struct Connected { serial: String }
struct Device<S> {
inner: DeviceInner,
_state: std::marker::PhantomData<S>,
}
impl Device<Disconnected> {
fn connect(self, serial: String) -> Result<Device<Connected>, AppError> {
// ...
}
}
impl Device<Connected> {
fn transfer(&self, file: &Path) -> Result<(), AppError> {
// Can only call this on a connected device
// Device<Disconnected> doesn't have this method
}
}
transfer() on a disconnected device is a compile error, not a runtime panic. No guard clause needed.
The Verdict
Ownership fights get easier with patterns, not by avoiding Rust's rules.
Clone cheaply, reference when it matters, Arc + Mutex for shared state, builder pattern for complex construction, type states for correctness. These cover 90% of what comes up in real Tauri apps.
If this was useful, a ❤️ helps more than you'd think!
👉 HiyokoKit → https://hiyokomtp.lemonsqueezy.com/checkout/buy/2c94dd0f-e28a-4a17-8efc-7bd93087d46d
X → @hiyoyok
Top comments (1)
TL;DR: