Xilem Patterns and Best Practices
Component Pattern
Structure your app as composable components:
rust
use xilem::view::{flex_col, label, text_button};
use xilem::{WidgetView, core::Edit};
// Component: A function that takes state and returns a view
fn counter_component(count: i32, on_increment: impl Fn(&mut AppState) + Send + Sync + 'static)
-> impl WidgetView<Edit<AppState>>
{
flex_col((
label(format!("Count: {}", count)),
text_button("Increment", on_increment),
))
}
// Usage in parent
fn app_logic(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
counter_component(state.count, |s| s.count += 1)
}
State Management
Centralized State
rust
struct AppState {
user: UserState,
todos: Vec<Todo>,
ui: UiState,
}
struct UiState {
filter: Filter,
editing_id: Option<usize>,
}
Using Lens for Modular Components
rust
use xilem::core::lens;
fn app_logic(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
flex_col((
// User component only sees UserState
lens(user_profile, |s: &mut AppState, ()| &mut s.user),
// Todo list only sees Vec<Todo>
lens(todo_list, |s: &mut AppState, ()| &mut s.todos),
))
}
List Rendering
Static Lists
rust
fn todo_list(todos: &mut Vec<Todo>) -> impl WidgetView<Edit<Vec<Todo>>> + use<> {
let items: Vec<_> = todos
.iter()
.enumerate()
.map(|(i, todo)| {
todo_item(i, todo.clone())
})
.collect();
flex_col(items)
}
Filtered Lists
rust
fn filtered_todos(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
let items: Vec<_> = state.todos
.iter()
.enumerate()
.filter(|(_, todo)| match state.filter {
Filter::All => true,
Filter::Active => !todo.done,
Filter::Completed => todo.done,
})
.map(|(i, todo)| todo_item_view(i, todo))
.collect();
flex_col(items)
}
Async Operations
Using task for One-shot Operations
rust
use xilem::view::task;
fn data_loader(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
flex_col((
if state.loading {
Either::A(spinner())
} else {
Either::B(data_display(&state.data))
},
task(
|proxy| async move {
let data = fetch_data().await;
proxy.send_message(LoadComplete(data));
},
|state: &mut AppState, msg: LoadComplete| {
state.data = msg.0;
state.loading = false;
},
),
))
}
Using worker for Continuous Processing
rust
use xilem::view::worker;
fn search_view(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
flex_col((
text_input(state.query.clone(), |s, v| s.query = v),
worker(
state.query.clone(),
|query| async move {
search_api(&query).await
},
|state: &mut AppState, results| {
state.results = results;
},
),
results_list(&state.results),
))
}
Conditional Rendering
Using Option
rust
fn maybe_error(error: &Option<String>) -> Option<impl WidgetView<Edit<AppState>>> {
error.as_ref().map(|e| label(format!("Error: {}", e)))
}
// In flex_col, Option<impl View> is valid
flex_col((
main_content(),
maybe_error(&state.error),
))
Using Either/OneOf
rust
use xilem::core::one_of::Either;
fn conditional_view(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
if state.logged_in {
Either::A(dashboard(state))
} else {
Either::B(login_form(state))
}
}
Performance Optimization
Memoization
rust
use xilem::core::memoize;
fn expensive_view(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
flex_col((
// Only rebuilds when data changes
memoize(state.data.clone(), |data| {
expensive_computation_view(data)
}),
// Always rebuilds
dynamic_controls(state),
))
}
Frozen for Static Content
rust
use xilem::core::frozen;
fn app_logic(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
flex_col((
// Never rebuilds
frozen(|| header()),
// Dynamic content
main_content(state),
frozen(|| footer()),
))
}
Error Handling
rust
fn fallible_view(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
match &state.result {
Ok(data) => Either::A(data_view(data)),
Err(e) => Either::B(error_view(e)),
}
}
Form Patterns
rust
struct FormState {
name: String,
email: String,
errors: HashMap<String, String>,
}
fn form_view(form: &mut FormState) -> impl WidgetView<Edit<FormState>> + use<> {
flex_col((
text_input(form.name.clone(), |f, v| f.name = v)
.placeholder("Name"),
form.errors.get("name").map(|e| label(e.clone())),
text_input(form.email.clone(), |f, v| f.email = v)
.placeholder("Email"),
form.errors.get("email").map(|e| label(e.clone())),
text_button("Submit", |f: &mut FormState| {
f.errors.clear();
if f.name.is_empty() {
f.errors.insert("name".into(), "Name required".into());
}
if !f.email.contains('@') {
f.errors.insert("email".into(), "Invalid email".into());
}
}),
))
}
Multiple Windows
rust
use xilem::{window, WindowOptions};
fn app_logic(state: &mut AppState) -> impl WidgetView<Edit<AppState>> + use<> {
(
window(main_window_content(state), WindowOptions::new("Main")),
state.show_settings.then(|| {
window(settings_content(state), WindowOptions::new("Settings"))
}),
)
}