| name | code:rust-dioxus |
| description | Build Dioxus desktop GUI apps in Rust. Signals, components, hooks, async patterns, RSX, and state management.
<example>
Context: User is building a Dioxus app
user: "add a search component to this dioxus app"
</example>
<example>
Context: User needs Dioxus patterns
user: "how do I manage global state in dioxus"
</example>
|
Rust GUI Development (Dioxus)
Best practices for Rust desktop applications using Dioxus.
Project Structure
project/
├── Cargo.toml # Workspace root
├── src/ # Library crate (shared types, clients)
│ └── lib.rs
├── desktop/ # GUI crate (isolated)
│ ├── Cargo.toml
│ ├── Dioxus.toml # Window settings, asset dir
│ ├── assets/
│ │ └── tailwind.css
│ └── src/
│ ├── main.rs # Entry, global hooks
│ ├── state.rs # All Signal<T> definitions
│ └── components/ # UI components
Key Rule: Desktop GUI is a separate crate. Share only types and clients.
Dependencies
[dependencies]
dioxus = { version = "0.6", features = ["desktop"] }
dioxus-primitives = { git = "https://github.com/DioxusLabs/components" }
dioxus-free-icons = { version = "0.10", features = ["font-awesome-solid", "lucide"] }
[dev-dependencies]
dioxus-ssr = "0.6"
wiremock = "0.6"
Primitives First
Always use dioxus-primitives components. Don't build custom versions.
| Need | Use |
|---|
| Modal | Dialog, DialogContent, DialogTitle |
| Dropdown | Popover, PopoverTrigger, PopoverContent |
| Progress | Progress, ProgressIndicator |
| Tooltips | Tooltip, TooltipTrigger, TooltipContent |
| Toggle | Switch, SwitchThumb |
State Management
Global State with Signals
#[derive(Clone, Copy)]
pub struct AppState {
pub items: Signal<Vec<Item>>,
pub loading: Signal<bool>,
pub error: Signal<Option<String>>,
pub selected_ids: Signal<HashSet<String>>,
}
impl AppState {
pub fn new() -> Self {
Self {
items: Signal::new(vec![]),
loading: Signal::new(false),
error: Signal::new(None),
selected_ids: Signal::new(HashSet::new()),
}
}
}
Context Provider Pattern
fn app() -> Element {
let state = use_context_provider(AppState::new);
rsx! {
Router::<Route> {}
}
}
fn SomeComponent() -> Element {
let mut state = use_context::<AppState>();
}
Signal Access Rules
let items = state.items.read().clone();
let is_loading = *state.loading.read();
state.items.set(new_items);
{
let mut items = state.items.write();
items.insert(key, value);
}
Critical: Never hold signal locks across .await points.
Component Patterns
Props Need Traits
#[derive(Clone, PartialEq)]
pub struct ItemDisplay {
pub id: String,
pub name: String,
}
Basic Component
#[component]
pub fn ItemCard(item: ItemDisplay) -> Element {
rsx! {
div { class: "card",
h3 { "{item.name}" }
p { "ID: {item.id}" }
}
}
}
Component with Callbacks
#[component]
pub fn SearchBar(on_search: EventHandler<String>) -> Element {
let mut query = use_signal(String::new);
rsx! {
input {
value: "{query}",
oninput: move |e| query.set(e.value()),
onkeypress: move |e| {
if e.key() == Key::Enter {
on_search.call(query.read().clone());
}
}
}
}
}
SearchBar { on_search: move |q| handle_search(q) }
Event Handler with Async
let handle_click = {
let item = item.clone();
move |evt: MouseEvent| {
evt.stop_propagation();
spawn(async move {
state.loading.set(true);
match client.fetch(&item).await {
Ok(data) => state.data.set(data),
Err(e) => state.error.set(Some(e.to_string())),
}
state.loading.set(false);
});
}
};
Conditional Rendering
rsx! {
if *state.loading.read() {
LoadingSpinner {}
} else if let Some(err) = state.error.read().as_ref() {
ErrorBanner { message: err.clone() }
} else if state.items.read().is_empty() {
EmptyState { message: "No items" }
} else {
for item in state.items.read().iter() {
ItemCard { key: "{item.id}", item: item.clone() }
}
}
}
Hooks
use_hook(|| {
spawn(async move {
if let Ok(config) = Config::load() {
state.config.set(Some(config));
}
});
});
let filtered = use_memo(move || {
state.items.read()
.iter()
.filter(|i| i.matches(&state.filter.read()))
.cloned()
.collect::<Vec<_>>()
});
use_effect(move || {
let query = state.search_query.read().clone();
if query.len() >= 3 {
spawn(async move { });
}
});
Async Patterns
spawn(async move {
let result = client.fetch().await;
state.data.set(result);
});
tokio::spawn(async move {
state.data.set(result);
});
Critical Rules
- Signals are truth - All mutable state through
Signal<T>
- Scope locks tight -
{ let x = signal.read(); } then drop
- Clone before async -
let item = item.clone(); move |_| spawn(...)
- Spawn don't block - Never
.await in component render
- Key your lists -
for item in items { Card { key: "{item.id}" } }
- EventHandler for callbacks - Parent-child via
EventHandler<T>
- Context over props - Shared state via
use_context
Forbidden
.await in component body
- Holding signal locks across await
tokio::spawn for UI updates
- Prop drilling beyond 2 levels
- Custom components when primitives exist
Development
cd desktop && dx serve
Summary
| Pattern | Use |
|---|
| Global state | AppState with Signal<T> fields |
| Component state | use_signal() |
| Derived state | use_memo() |
| Side effects | use_effect() |
| Async work | spawn(async move { ... }) |
| Shared state | use_context::<AppState>() |