use dioxus::prelude::*;
use dioxus_provider::prelude::*;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::time::{Duration, sleep};
static SIMULATE_ERRORS: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Item {
pub id: u64,
pub name: String,
}
#[derive(Debug, thiserror::Error, Clone, PartialEq)]
pub enum ItemError {
#[error("Item not found")]
NotFound,
#[error("Other error: {0}")]
Other(String),
}
#[provider]
pub async fn load_items() -> Result<Vec<Item>, ItemError> {
println!("🔄 [LOAD_ITEMS] Provider is executing - returning fresh data");
Ok(vec![
Item {
id: 1,
name: "Item 1".to_string(),
},
Item {
id: 2,
name: "Item 2".to_string(),
},
Item {
id: 3,
name: "Item 3".to_string(),
},
])
}
#[mutation(
invalidates = [load_items],
optimistic = |items: &mut Vec<Item>, id: &u64| items.retain(|item| item.id != *id)
)]
pub async fn delete_item(_id: u64, items: Vec<Item>) -> Result<Vec<Item>, ItemError> {
sleep(Duration::from_millis(1000)).await;
if SIMULATE_ERRORS.load(Ordering::Relaxed) {
return Err(ItemError::Other(
"Simulated server error - deletion rejected!".to_string(),
));
}
Ok(items)
}
#[mutation(
invalidates = [load_items],
optimistic = |items: &mut Vec<Item>, id: &u64, new_name: &String| {
if let Some(item) = items.iter_mut().find(|i| i.id == *id) {
item.name = new_name.clone();
}
}
)]
pub async fn update_item(
_id: u64,
_new_name: String,
items: Vec<Item>,
) -> Result<Vec<Item>, ItemError> {
sleep(Duration::from_millis(1000)).await;
if SIMULATE_ERRORS.load(Ordering::Relaxed) {
return Err(ItemError::Other(
"Simulated server error - update rejected!".to_string(),
));
}
Ok(items)
}
#[component]
pub fn ItemCard(item: Item) -> Element {
let (delete_state, delete_item) = use_mutation(delete_item());
let (update_state, update_item) = use_mutation(update_item());
let mut new_name = use_signal(|| item.name.clone());
let item_id = item.id;
let on_delete = move |_| {
delete_item(item_id);
};
let on_update = move |_| {
let name = new_name.read().clone();
update_item((item_id, name));
};
rsx! {
div {
style: "border: 1px solid #ccc; padding: 10px; margin: 5px;",
div {
style: "margin-bottom: 5px;",
strong { "ID: {item.id} - " }
span { "{item.name}" }
}
div {
style: "display: flex; gap: 5px; align-items: center;",
input {
r#type: "text",
value: "{new_name}",
oninput: move |evt| new_name.set(evt.value().clone())
}
button { onclick: on_update, "Update" }
button { onclick: on_delete, "Delete" }
}
if let MutationState::Error(err) = &*delete_state.read() {
div { style: "color: red; margin-top: 5px;", "Delete error: {err}" }
}
if let MutationState::Error(err) = &*update_state.read() {
div { style: "color: red; margin-top: 5px;", "Update error: {err}" }
}
}
}
}
#[component]
pub fn ItemsList() -> Element {
let items = use_provider(load_items(), ());
rsx! {
div {
h2 { "Items List" }
match &*items.read() {
State::Loading { .. } => rsx! {
div { "Loading..." }
},
State::Error(err) => rsx! {
div { "Error: {err}" }
},
State::Success(items) => {
if items.is_empty() {
rsx! {
div { "No items" }
}
} else {
rsx! {
div {
for item in items {
ItemCard { item: item.clone() }
}
}
}
}
}
}
}
}
}
#[component]
pub fn App() -> Element {
let mut simulate_errors = use_signal(|| false);
let toggle_errors = move |_| {
let new_value = !simulate_errors();
simulate_errors.set(new_value);
SIMULATE_ERRORS.store(new_value, Ordering::Relaxed);
};
rsx! {
div {
style: "font-family: sans-serif; max-width: 600px; margin: 20px auto; padding: 20px;",
h1 { "Optimistic Mutations Demo" }
div {
style: "background: #f0f0f0; padding: 15px; margin: 15px 0; border-radius: 5px;",
p { "This demo shows both single-arg and multi-arg optimistic mutations:" }
ul {
li { "Delete: Single-arg optimistic mutation - item disappears INSTANTLY" }
li { "Update: Multi-arg optimistic mutation - name changes INSTANTLY" }
}
p {
style: "margin-bottom: 0;",
"No loading states, no waiting - just immediate feedback!"
}
}
div {
style: "background: #fff3cd; padding: 15px; margin: 15px 0; border-radius: 5px; border: 1px solid #ffc107;",
label {
style: "display: flex; align-items: center; gap: 10px; cursor: pointer; font-weight: bold;",
input {
r#type: "checkbox",
checked: simulate_errors(),
onchange: toggle_errors,
style: "width: 20px; height: 20px; cursor: pointer;"
}
span {
if simulate_errors() {
"✓ Simulate Errors (mutations will fail and rollback)"
} else {
"☐ Simulate Errors (enable to see automatic rollback)"
}
}
}
if simulate_errors() {
p {
style: "margin: 10px 0 0 0; color: #856404;",
"⚠️ Try deleting or updating items - they will appear to change, then rollback with an error!"
}
}
}
ItemsList {}
}
}
}
fn main() {
let _ = dioxus_provider::init();
dioxus::launch(App);
}