The RSX macro makes it very easy to assemble complex UIs with a very natural Rust syntax:
rsx!(
div {
button {
onclick: move |e| todos.write().new_todo(),
"Add todo"
}
ul {
class: "todo-list",
todos.iter().map(|(key, todo)| rsx!(
li {
class: "beautiful-todo"
key: "f"
h3 { "{todo.title}" }
p { "{todo.contents}"}
}
))
}
}
)
In this section, we'll cover the rsx!
macro in depth. If you prefer to learn through examples, the code reference
guide has plenty of examples on how to use rsx!
effectively.
Attributes must come before child elements
div {
hidden: "false",
"some text"
child {}
Component {} // uppercase
component() // lowercase is treated like a function call
(0..10).map(|f| rsx!{ "hi {f}" }) // arbitrary rust expressions
}
Each element takes a comma-separated list of expressions to build the node. Roughly, here's how they work:
name: value
sets a property on this element."text"
adds a new text elementtag {}
adds a new child elementCustomTag {}
adds a new child component{expr}
pastes the expr
tokens literally. They must be IntoIterator<T> where T: IntoVnode
to work properlyCommas are entirely optional, but might be useful to delineate between elements and attributes.
The render
function provides an extremely efficient allocator for VNodes and text, so try not to use the format!
macro in your components. Rust's default ToString
methods pass through the global allocator, but all text in components is allocated inside a manually-managed Bump arena. To push you in the right direction, all text-based attributes take std::fmt::Arguments
directly, so you'll want to reach for format_args!
when the built-in f-string
interpolation just doesn't cut it.
cx.render
with rsx!(cx, ...)
Sometimes, writing cx.render
is a hassle. The `rsx! macro will accept any token followed by a comma as the target to call "render" on:
cx.render(rsx!( div {} ))
// becomes
rsx!(cx, div {})
Sometimes, you might not want to render an element given a condition. The rsx! macro will accept any tokens directly contained with curly braces, provided they resolve to a type that implements IntoIterator<VNode>
. This lets us write any Rust expression that resolves to a VNode:
rsx!({
if enabled {
rsx!(cx, div {"enabled"})
} else {
rsx!(cx, li {"disabled"})
}
})
A convenient way of hiding/showing an element is returning an Option<VNode>
. When combined with and_then
, we can succinctly control the display state given some boolean:
rsx!({
a.and_then(rsx!(div {"enabled"}))
})
It's important to note that the expression rsx!()
is typically lazy - this expression must be rendered to produce a VNode. When using match statements, we must render every arm as to avoid the no two closures are identical
rule that Rust imposes:
// this will not compile!
match case {
true => rsx!(div {}),
false => rsx!(div {})
}
// the nodes must be rendered first
match case {
true => rsx!(cx, div {}),
false => rsx!(cx, div {})
}
Again, because anything that implements IntoIterator<VNode>
is valid, we can use lists directly in our rsx!
:
let items = vec!["a", "b", "c"];
cx.render(rsx!{
ul {
{items.iter().map(|f| rsx!(li { "a" }))}
}
})
Sometimes, it makes sense to render VNodes into a list:
let mut items = vec![];
for _ in 0..5 {
items.push(rsx!(cx, li {} ))
}
rsx!(cx, {items} )
When rendering the VirtualDom to the screen, Dioxus needs to know which elements have been added and which have been removed. These changes are determined through a process called "diffing" - an old set of elements is compared to a new set of elements. If an element is removed, then it won't show up in the new elements, and Dioxus knows to remove it.
However, with lists, Dioxus does not exactly know how to determine which elements have been added or removed if the order changes or if an element is added or removed from the middle of the list.
In these cases, it is vitally important to specify a "key" alongside the element. Keys should be persistent between renders.
fn render_list(cx: Scope, items: HashMap<String, Todo>) -> DomTree {
rsx!(cx, ul {
{items.iter().map(|key, item| {
li {
key: key,
h2 { "{todo.title}" }
p { "{todo.contents}" }
}
})}
})
}
There have been many guides made for keys in React, so we recommend reading up to understand their importance:
let text = "example";
cx.render(rsx!{
div {
h1 { "Example" },
{title}
// fstring interpolation
"{text}"
p {
// Attributes
tag: "type",
// Anything that implements display can be an attribute
abc: 123,
enabled: true,
// attributes also supports interpolation
// `class` is not a restricted keyword unlike JS and ClassName
class: "big small wide short {text}",
class: format_args!("attributes take fmt::Arguments. {}", 99),
tag: {"these tokens are placed directly"}
// Children
a { "abcder" },
// Children with attributes
h2 { "hello", class: "abc-123" },
// Child components
CustomComponent { a: 123, b: 456, key: "1" },
// Child components with paths
crate::components::CustomComponent { a: 123, b: 456, key: "1" },
// Iterators
{ (0..3).map(|i| rsx!( h1 {"{:i}"} )) },
// More rsx!, or even html!
{ rsx! { div { } } },
{ html! { <div> </div> } },
// Matching
// Requires rendering the nodes first.
// rsx! is lazy, and the underlying closures cannot have the same type
// Rendering produces the VNode type
{match rand::gen_range::<i32>(1..3) {
1 => rsx!(cx, h1 { "big" })
2 => rsx!(cx, h2 { "medium" })
_ => rsx!(cx, h3 { "small" })
}}
// Optionals
{true.and_then(|f| rsx!( h1 {"Conditional Rendering"} ))}
// Child nodes
{cx.props.children}
// Any expression that is `IntoVNode`
{expr}
}
}
})