You will often want to display multiple similar components from a collection of data.
In this chapter, you will learn:
rsx!
Thinking back to our analysis of the r/reddit
page, we notice a list of data that needs to be rendered: the list of posts. This list of posts is always changing, so we cannot just hardcode the lists into our app like:
rsx!(
div {
Post {/* some properties */}
Post {/* some properties */}
Post {/* some properties */}
}
)
Instead, we need to transform the list of data into a list of Elements.
For convenience, rsx!
supports any type in curly braces that implements the IntoVnodeList
trait. Conveniently, every iterator that returns something that can be rendered as an Element also implements IntoVnodeList
.
As a simple example, let's render a list of names. First, start with our input data:
let names = ["jim", "bob", "jane", "doe"];
Then, we create a new iterator by calling iter
and then map
. In our map
function, we'll place render our template.
let name_list = names.iter().map(|name| rsx!(
li { "{name}" }
));
Finally, we can include this list in the final structure:
rsx!(
ul {
{name_list}
}
)
The HTML-rendered version of this list would follow what you would expect:
<ul>
<li> jim </li>
<li> bob </li>
<li> jane </li>
<li> doe </li>
</ul>
Let's start by modeling this problem with a component and some properties.
For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List.
#[derive(Props, PartialEq)]
struct PostListProps<'a> {
posts: &'a [PostData]
}
Next, we're going to define our component:
fn App(cx: Scope<PostList?) -> Element {
// First, we create a new iterator by mapping the post array
let posts = cx.props.posts.iter().map(|post| rsx!{
Post {
title: post.title,
age: post.age,
original_poster: post.original_poster
}
});
// Finally, we render the post list inside of a container
cx.render(rsx!{
ul { class: "post-list"
{posts}
}
})
}
Rust's iterators are extremely powerful, especially when used for filtering tasks. When building user interfaces, you might want to display a list of items filtered by some arbitrary check.
As a very simple example, let's set up a filter where we only list names that begin with the letter "J".
Let's make our list of names:
let names = ["jim", "bob", "jane", "doe"];
Then, we create a new iterator by calling iter
, then filter
, then map
. In our filter
function, we'll only allow "j" names, and in our map
function, we'll render our template.
let name_list = names
.iter()
.filter(|name| name.starts_with('j'))
.map(|name| rsx!( li { "{name}" }));
Rust's iterators provide us tons of functionality and are significantly easier to work with than JavaScript's map/filter/reduce.
For keen Rustaceans: notice how we don't actually call collect
on the name list. If we collected
our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new lazy iterator which will then be consumed by Dioxus in the render
call.
The render
method is extraordinarily efficient, so it's best practice to let it do most of the allocations for us.
The examples above demonstrate the power of iterators in rsx!
but all share the same issue: a lack of "keys". Whenever you render a list of elements, each item in the list must be uniquely identifiable. To make each item unique, you need to give it a "key".
In Dioxus, keys are strings that uniquely identifies it among other items in that array:
rsx!( li { key: "a" } )
Keys tell Dioxus which array item each component corresponds to, so that it can match them up later. This becomes important if your array items can move (e.g. due to sorting), get inserted, or get deleted. A well-chosen key helps Dioxus infer what exactly has happened, and make the correct updates to the screen
NB: the language from this section is strongly borrowed from React's guide on keys.
Different sources of data provide different sources of keys:
uuid
when creating items.Imagine that files on your desktop didn’t have names. Instead, you’d refer to them by their order — the first file, the second file, and so on. You could get used to it, but once you delete a file, it would get confusing. The second file would become the first file, the third file would be the second file, and so on.
File names in a folder and Element keys in an array serve a similar purpose. They let us uniquely identify an item between its siblings. A well-chosen key provides more information than the position within the array. Even if the position changes due to reordering, the key lets Dioxus identify the item throughout its lifetime.
You might be tempted to use an item’s index in the array as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered. Index as a key often leads to subtle and confusing bugs.
Similarly, do not generate keys on the fly, gen_random
. This will cause keys to never match up between renders, leading to all your components and DOM being recreated every time. Not only is this slow, but it will also lose any user input inside the list items. Instead, use a stable ID based on the data.
Note that your components won’t receive key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop:
Post { key: "{key}", id: "{id}" }
In this section, we learned:
Moving forward, we'll finally cover user input and interactivity.