Optimistic updates
Waiting for the server before showing a change makes the app feel slow. With optimistic updates the change is shown right away, and fixed later only if the server didn't accept it.
Single value: LazyCache
optimistically_change (or optimistically_set) replaces the cached value without a request. Then send the change, and reload the value from the server with force_update:
use vertigo::{bind, dom, get_driver, transaction, AutoJsJson, RequestBuilder, Resource}; #[derive(AutoJsJson, Clone, PartialEq)] pub struct Settings { newsletter: bool, } let settings = RequestBuilder::get("/api/settings").lazy_cache(|status, body| { if status == 200 { Some(body.into::<Settings>()) } else { None } }); let toggle = bind!(settings, |_| { // 1. Show the change right away settings.optimistically_change(|settings| settings.newsletter = !settings.newsletter); let Resource::Ready(new_settings) = transaction(|ctx| settings.get(ctx)) else { return; }; // 2. Send it to the server get_driver().spawn(bind!(settings, async move { let response = RequestBuilder::put("/api/settings") .body_json((*new_settings).clone()) .call() .await; // 3. Reload from the server: silently if saved, with loading state if not settings.force_update(response.status() != Some(200)); })); }); settings.render(move |settings| { let state = if settings.newsletter { "on" } else { "off" }; dom! { <p> "Newsletter: " {state} " " <button on_click={toggle.clone()}>"Toggle"</button> </p> } })
List: LazyListCache
For lists use LazyListCache, which keeps every item separately and can undo changes of single items. Its methods don't send any requests, they only change what the user sees:
optimistically_set_item- adds or changes an item,optimistically_remove_item- hides an item,
and when the server responds:
update_item/update_item_with_old_key- saves the item received from the server,remove_item- removes the item for good,rollback- undoes the change (a never saved item gets removed).
use vertigo::{bind, dom, get_driver, AutoJsJson, CollectionKey, RequestBuilder}; #[derive(AutoJsJson, Clone, PartialEq)] pub struct Todo { id: u32, title: String, } #[derive(AutoJsJson)] pub struct NewTodo { title: String, } #[derive(PartialEq)] pub struct TodoKey; impl CollectionKey for TodoKey { type Key = u32; type Value = Todo; fn get_key(todo: &Todo) -> u32 { todo.id } } // Temporary id of a todo not saved yet const DRAFT_ID: u32 = 0; let todos = RequestBuilder::get("/api/todos").lazy_list_cache::<TodoKey>(|status, body| { if status == 200 { Some(body.into::<Vec<Todo>>()) } else { None } }); let add = bind!(todos, |_| { let title = "New todo".to_string(); // Shown right away, under the temporary id todos.optimistically_set_item(Todo { id: DRAFT_ID, title: title.clone(), }); get_driver().spawn(bind!(todos, async move { let response = RequestBuilder::post("/api/todos") .body_json(NewTodo { title }) .call() .await; match response.into(|status, body| (status == 201).then(|| body.into::<Todo>())) { // Replaces the draft with the saved todo, keeping its position Ok(todo) => todos.update_item_with_old_key(&DRAFT_ID, todo), // Removes the draft Err(_) => todos.rollback(&DRAFT_ID), } })); }); let list = todos.render(bind!(todos, |todos_list| { dom! { <ul> {..todos_list.iter().map(|todo| { let id = todo.id; let remove = bind!(todos, |_| { // Hidden right away todos.optimistically_remove_item(&id); get_driver().spawn(bind!(todos, async move { let response = RequestBuilder::delete(format!("/api/todos/{id}")) .call() .await; if response.status() == Some(204) { todos.remove_item(&id); } else { // Brings the todo back todos.rollback(&id); } })); }); dom! { <li>{&todo.title} " " <button on_click={remove}>"Remove"</button></li> } })} </ul> } })); dom! { <div> {list} <button on_click={add}>"Add"</button> </div> }
See the LazyListCache guide for more.