DEV Community

Cover image for Rust Ownership Made Practical — Patterns I Use in Every Tauri App
hiyoyo
hiyoyo

Posted on

Rust Ownership Made Practical — Patterns I Use in Every Tauri App

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]) { ... }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
hiyoyok profile image
hiyoyo

TL;DR:

  • Clone String/PathBuf freely, don't clone large structs or hot-loop data
  • Arc for shared ownership across threads — clone the Arc, not the data
  • Arc> for mutable shared state — keep lock scope small
  • Builder pattern for structs with many optional fields
  • Type states encode valid state in the type system — invalid transitions become compile errors