Control Flow
In most applications, you sometimes need to make a decision: Should I render this
part of the view, or not? Should I render <ButtonA/>
or <WidgetB/>
? This is
control flow.
A Few Tips
When thinking about how to do this with Leptos, it’s important to remember a few things:
- Rust is an expression-oriented language: control-flow expressions like
if x() { y } else { z }
andmatch x() { ... }
return their values. This makes them very useful for declarative user interfaces. - For any
T
that implementsIntoView
—in other words, for any type that Leptos knows how to render—Option<T>
andResult<T, impl Error>
also implementIntoView
. And just asFn() -> T
renders a reactiveT
,Fn() -> Option<T>
andFn() -> Result<T, impl Error>
are reactive. - Rust has lots of handy helpers like Option::map,
Option::and_then,
Option::ok_or,
Result::map,
Result::ok, and
bool::then that
allow you to convert, in a declarative way, between a few different standard types,
all of which can be rendered. Spending time in the
Option
andResult
docs in particular is one of the best ways to level up your Rust game. - And always remember: to be reactive, values must be functions. You’ll see me constantly
wrap things in a
move ||
closure, below. This is to ensure that they actually rerun when the signal they depend on changes, keeping the UI reactive.
So What?
To connect the dots a little: this means that you can actually implement most of your control flow with native Rust code, without any control-flow components or special knowledge.
For example, let’s start with a simple signal and derived signal:
let (value, set_value) = signal(0);
let is_odd = move || value.get() % 2 != 0;
We can use these signals and ordinary Rust to build most control flow.
if
statements
Let’s say I want to render some text if the number is odd, and some other text if it’s even. Well, how about this?
view! {
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
}
An if
expression returns its value, and a &str
implements IntoView
, so a
Fn() -> &str
implements IntoView
, so this... just works!
Option<T>
Let’s say we want to render some text if it’s odd, and nothing if it’s even.
let message = move || {
if is_odd() {
Some("Ding ding ding!")
} else {
None
}
};
view! {
<p>{message}</p>
}
This works fine. We can make it a little shorter if we’d like, using bool::then()
.
let message = move || is_odd().then(|| "Ding ding ding!");
view! {
<p>{message}</p>
}
You could even inline this if you’d like, although personally I sometimes like the
better cargo fmt
and rust-analyzer
support I get by pulling things out of the view
.
match
statements
We’re still just writing ordinary Rust code, right? So you have all the power of Rust’s pattern matching at your disposal.
let message = move || {
match value.get() {
0 => "Zero",
1 => "One",
n if is_odd() => "Odd",
_ => "Even"
}
};
view! {
<p>{message}</p>
}
And why not? YOLO, right?
Preventing Over-Rendering
Not so YOLO.
Everything we’ve just done is basically fine. But there’s one thing you should remember and try to be careful with. Each one of the control-flow functions we’ve created so far is basically a derived signal: it will rerun every time the value changes. In the examples above, where the value switches from even to odd on every change, this is fine.
But consider the following example:
let (value, set_value) = signal(0);
let message = move || if value.get() > 5 {
"Big"
} else {
"Small"
};
view! {
<p>{message}</p>
}
This works, for sure. But if you added a log, you might be surprised
let message = move || if value.get() > 5 {
logging::log!("{}: rendering Big", value());
"Big"
} else {
logging::log!("{}: rendering Small", value());
"Small"
};
As a user clicks a button, you’d see something like this:
1: rendering Small
2: rendering Small
3: rendering Small
4: rendering Small
5: rendering Small
6: rendering Big
7: rendering Big
8: rendering Big
... ad infinitum
Every time value
changes, it reruns the if
statement. This makes sense, with
how reactivity works. But it has a downside. For a simple text node, rerunning
the if
statement and rerendering isn’t a big deal. But imagine it were
like this:
let message = move || if value.get() > 5 {
<Big/>
} else {
<Small/>
};
This rerenders <Small/>
five times, then <Big/>
infinitely. If they’re
loading resources, creating signals, or even just creating DOM nodes, this is
unnecessary work.
<Show/>
The <Show/>
component is
the answer. You pass it a when
condition function, a fallback
to be shown if
the when
function returns false
, and children to be rendered if when
is true
.
let (value, set_value) = signal(0);
view! {
<Show
when=move || { value.get() > 5 }
fallback=|| view! { <Small/> }
>
<Big/>
</Show>
}
<Show/>
memoizes the when
condition, so it only renders its <Small/>
once,
continuing to show the same component until value
is greater than five;
then it renders <Big/>
once, continuing to show it indefinitely or until value
goes below five and then renders <Small/>
again.
This is a helpful tool to avoid rerendering when using dynamic if
expressions.
As always, there's some overhead: for a very simple node (like updating a single
text node, or updating a class or attribute), a move || if ...
will be more
efficient. But if it’s at all expensive to render either branch, reach for
<Show/>
.
Note: Type Conversions
There‘s one final thing it’s important to say in this section.
Leptos uses a statically-typed view tree. The view
macro returns different types
for different kinds of view.
This won’t compile, because the different HTML elements are different types.
view! {
<main>
{move || match is_odd() {
true if value.get() == 1 => {
view! { <pre>"One"</pre> }
},
false if value.get() == 2 => {
view! { <p>"Two"</p> }
}
// returns HtmlElement<Textarea>
_ => view! { <textarea>{value.get()}</textarea> }
}}
</main>
}
This strong typing is very powerful, because it enables all sorts of compile-time optimizations. But it can be a little annoying in conditional logic like this, because you can’t return different types from different branches of a condition in Rust. There are two ways to get yourself out of this situation:
- Use the enum
Either
(andEitherOf3
,EitherOf4
, etc.) to convert the different types to the same type. - Use
.into_any()
to convert multiple types into one typed-erasedAnyView
.
Here’s the same example, with the conversion added:
view! {
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { <p>"Two"</p> }.into_any()
}
// returns HtmlElement<Textarea>
_ => view! { <textarea>{value()}</textarea> }.into_any()
}}
</main>
}
Live example
CodeSandbox Source
use leptos::prelude::*;
#[component]
fn App() -> impl IntoView {
let (value, set_value) = signal(0);
let is_odd = move || value.get() & 1 == 1;
let odd_text = move || if is_odd() {
Some("How odd!")
} else {
None
};
view! {
<h1>"Control Flow"</h1>
// Simple UI to update and show a value
<button on:click=move |_| *set_value.write() += 1>
"+1"
</button>
<p>"Value is: " {value}</p>
<hr/>
<h2><code>"Option<T>"</code></h2>
// For any `T` that implements `IntoView`,
// so does `Option<T>`
<p>{odd_text}</p>
// This means you can use `Option` methods on it
<p>{move || odd_text().map(|text| text.len())}</p>
<h2>"Conditional Logic"</h2>
// You can do dynamic conditional if-then-else
// logic in several ways
//
// a. An "if" expression in a function
// This will simply re-render every time the value
// changes, which makes it good for lightweight UI
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
// b. Toggling some kind of class
// This is smart for an element that's going to
// toggled often, because it doesn't destroy
// it in between states
// (you can find the `hidden` class in `index.html`)
<p class:hidden=is_odd>"Appears if even."</p>
// c. The <Show/> component
// This only renders the fallback and the child
// once, lazily, and toggles between them when
// needed. This makes it more efficient in many cases
// than a {move || if ...} block
<Show when=is_odd
fallback=|| view! { <p>"Even steven"</p> }
>
<p>"Oddment"</p>
</Show>
// d. Because `bool::then()` converts a `bool` to
// `Option`, you can use it to create a show/hide toggled
{move || is_odd().then(|| view! { <p>"Oddity!"</p> })}
<h2>"Converting between Types"</h2>
// e. Note: if branches return different types,
// you can convert between them with
// `.into_any()` (for different HTML element types)
// or `.into_view()` (for all view types)
{move || match is_odd() {
true if value.get() == 1 => {
// <pre> returns HtmlElement<Pre>
view! { <pre>"One"</pre> }.into_any()
},
false if value.get() == 2 => {
// <p> returns HtmlElement<P>
// so we convert into a more generic type
view! { <p>"Two"</p> }.into_any()
}
_ => view! { <textarea>{value.get()}</textarea> }.into_any()
}}
}
}
fn main() {
leptos::mount::mount_to_body(App)
}