Rendering lists
Spreading an iterator in dom! (see "Spreading iterables in HTML") is fine for static content. But if you wrap it in render_value of a reactive list, every change rebuilds all rows. For reactive lists use render_list, which keeps the rows identified by a key:
use vertigo::{bind, dom, render::render_list, Value}; #[derive(Clone, PartialEq)] struct Todo { id: u32, title: String, } let todos = Value::new(vec![ Todo { id: 1, title: "Learn vertigo".to_string(), }, Todo { id: 2, title: "Build an app".to_string(), }, ]); let list = render_list( &todos, // Key identifying the item |todo| todo.id, // Called once per key, when it appears on the list. // Changes of the item come later through the `Computed`. |todo| { let title = todo.map(|todo| todo.title); dom! { <li>{title}</li> } }, ); let add = bind!(todos, |_| todos.change(|todos| { let id = todos.len() as u32 + 1; todos.push(Todo { id, title: format!("Todo {id}"), }); })); let reverse = bind!(todos, |_| todos.change(|todos| todos.reverse())); let rename_first = bind!(todos, |_| todos.change(|todos| { if let Some(first) = todos.first_mut() { first.title.push('!'); } })); dom! { <div> <ul>{list}</ul> <button on_click={add}>"Add"</button> <button on_click={reverse}>"Reverse"</button> <button on_click={rename_first}>"Rename first"</button> </div> }
The render closure is called once per key, when the key appears on the list, and gets a Computed of the item. So in the example above:
- "Add" renders only the new row,
- "Reverse" moves existing rows instead of rebuilding them, so things like a typed-in input value survive,
- "Rename first" only updates the text of the first row, as the title is embedded into
dom!as aComputed.
The key needs to be stable and unique. Don't use the index of the item as a key - reversing such list would update the content of every row instead of moving them. Items with a duplicated key are skipped (with an error in the console).
Use the Computed
As the closure runs only once per key, the row must be rendered from the Computed, not from a value read out of it:
// Wrong: a plain read - the row renders once and then never updates let wrong = render_list( &todos, |todo| todo.id, |todo| { let todo = transaction(|ctx| todo.get(ctx)); dom! { <li>{todo.title}</li> } }, ); // Right: the row is rendered from the `Computed`, so it follows the changes let right = render_list( &todos, |todo| todo.id, |todo| todo.render_value(|todo| dom! { <li>{todo.title}</li> }), );
Embedding a mapped Computed (like title in the first example) updates just a text or an attribute. render_value rebuilds the whole row whenever the item changes, so use it when the structure of the row depends on the item.
Lists from LazyCache
When the list comes from the network, use render_resource_list_memo. Items are identified by a CollectionKey implemented on a marker type, and after the cache gets refreshed, only rows whose items actually changed are updated:
use vertigo::{ dom, render::render_resource_list_memo, AutoJsJson, CollectionKey, RequestBuilder, }; #[derive(AutoJsJson, Clone, PartialEq)] pub struct Post { id: u32, title: String, } // Describes how to identify items on the list struct PostKey; impl CollectionKey for PostKey { type Key = u32; type Value = Post; fn get_key(post: &Post) -> u32 { post.id } } let posts = RequestBuilder::get("https://some.api/posts") .ttl_seconds(300) .lazy_cache(|status, body| { if status == 200 { Some(body.into::<Vec<Post>>()) } else { None } }); // Loading and Error states are rendered as an empty list let list = render_resource_list_memo::<PostKey>(&posts, |post| { let title = post.map(|post| post.title); dom! { <li>{title}</li> } }); dom! { <ul>{list}</ul> }
There's also render_list_memo for a list kept in Value<Rc<Vec<T>>>. For lists edited by the user see LazyListCache.