Conditional rendering
In this chapter, you will:
- Show and hide views reactively with the
whenfunction- Chain conditions with
.or()and.otherwise()for multi-branch logic- Derive boolean conditions from signals using
.map()and.equal_to()- Pick between
whenandmatch+.anyview()for complex branching
Think about the screens in a typical app: a loading spinner while data fetches, a “Welcome back!” message when the user is logged in, a “Please log in” prompt when they are not. Your UI needs to show different things based on conditions that can change at any moment. WaterUI provides the when function for exactly this — unlike Rust’s built-in if/else (which evaluates once at build time), when creates reactive branches that automatically swap views as conditions change.
Basic usage
when takes a reactive boolean condition and a builder closure that returns the view to show when the condition is true:
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn maybe_message(show_message: &Binding<bool>) -> impl View {
when(show_message.clone(), || text("This message is visible!"))
}
}
When show_message is false, nothing is rendered. The UI updates automatically whenever the binding changes.
Adding a fallback with .otherwise()
Use .otherwise() to provide an alternative view when the condition is false:
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn login_state(is_logged_in: &Binding<bool>) -> impl View {
when(is_logged_in.clone(), || text("Welcome back!"))
.otherwise(|| text("Please log in"))
}
}
This is the reactive equivalent of an if/else expression — but it responds to signal changes at runtime.
Chaining conditions with .or()
For multi-branch logic (analogous to if/else if/else), chain .or() calls. Each .or() adds another conditional branch; the chain must end with .otherwise():
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn status_text(state: &Binding<i32>) -> impl View {
when(state.equal_to(0), || text("Loading..."))
.or(state.equal_to(1), || text("Ready"))
.or(state.equal_to(2), || text("Error"))
.otherwise(|| text("Unknown state"))
}
}
The first matching condition wins — subsequent branches are not evaluated.
Note: Think of this as a reactive
match. The conditions are checked in order, and only the first matching branch renders.
Condition types
when accepts any type that implements IntoComputed<bool>. In practice you will use a handful of common patterns.
Binding<bool>
The simplest case — a boolean binding directly:
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn visible(show: &Binding<bool>) -> impl View {
when(show.clone(), || text("Visible"))
}
}
Negated binding
Binding<bool> implements Not, which produces a new signal:
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn hidden(show: &Binding<bool>) -> impl View {
// Show only when the binding is `false`.
when(!show.clone(), || text("Hidden content revealed"))
}
}
Derived Computed<bool>
Any Computed<bool> works as a condition. Build one with SignalExt::map:
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn positive_indicator(count: &Binding<i32>) -> impl View {
let is_positive = count.map(|n| n > 0).computed();
when(is_positive, || text("Count is positive"))
}
}
Derived conditions with SignalExt
SignalExt ships with comparison helpers that produce Computed<bool> directly. They are the most readable way to turn a value into a condition:
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn name_status(name: &Binding<Str>) -> impl View {
when(name.is_empty(), || text("Please enter your name"))
.otherwise(|| text!("Hello, {name}!"))
}
}
.equal_to() for value comparison
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn tab_content(selected_tab: &Binding<i32>) -> impl View {
when(selected_tab.equal_to(0), || text("Home"))
.or(selected_tab.equal_to(1), || text("Settings"))
.otherwise(|| text("Unknown tab"))
}
}
Static bool
Plain bool values also work. When all conditions in a chain are static booleans, the framework picks the matching branch at construction time, so the unused branches never cost anything at runtime:
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn debug_only() -> impl View {
when(cfg!(debug_assertions), || text("Debug mode"))
.otherwise(|| text("Release mode"))
}
}
Tip: Use this pattern for feature flags and debug-only UI.
When to reach for .anyview() instead
when().or().otherwise() chains are great for two or three branches. For richer matching — especially when each arm constructs a different concrete view type — destructure the value with match and erase each arm with .anyview():
#![allow(unused)]
fn main() {
use waterui::prelude::*;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode { A, B, C }
fn render(mode: Mode) -> AnyView {
match mode {
Mode::A => text("Mode A").title().anyview(),
Mode::B => button("Mode B").action(|| {}).anyview(),
Mode::C => vstack((text("Header"), text("Body"))).anyview(),
}
}
}
Use .anyview() whenever you need uniform view types across branches and the boolean ladder of when is starting to feel like an enum match.
Rendering mechanics
Understanding how when works under the hood helps you write efficient conditional views. Internally, When uses the Dynamic view to swap content:
- The combined condition signal re-evaluates.
- The framework determines which branch index matched.
- The previous view is removed and the matching branch’s builder is called.
- The new view is inserted into the tree.
Each branch closure runs every time the condition switches into that branch, so keep them lightweight. State that should survive a branch toggle must live outside the branch — typically in a Binding owned by the parent.
Patterns and examples
Show / hide with a toggle
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn settings_panel() -> impl View {
let show_advanced = Binding::bool(false);
let value = Binding::f64(0.5);
vstack((
toggle("Show Advanced", &show_advanced),
when(show_advanced.clone(), {
let value = value.clone();
move || {
vstack((
text("Advanced Settings").headline(),
slider(&value).range(0.0..=1.0),
))
}
}),
))
}
}
Loading states
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn data_view(loading: &Binding<bool>, data: &Binding<Str>) -> impl View {
let data = data.clone();
when(loading.clone(), || text("Loading..."))
.otherwise(move || text!("{data}"))
}
}
Multi-state status indicator
#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::widget::condition::when;
fn status_indicator(status: &Binding<i32>) -> impl View {
when(status.equal_to(0), || text("Idle").color(Grey))
.or(status.equal_to(1), || text("Running").color(Green))
.or(status.equal_to(2), || text("Warning").color(Yellow))
.otherwise(|| text("Error").color(Red))
}
}
Best practices
- Always end chains with
.otherwise(). A barewhen()without.otherwise()renders nothing when the condition is false. Multi-branch chains require.otherwise()to close. - Use signal combinators, not
.get(). Calling.get()inside awhencondition or branch closure breaks reactivity. Prefer.map(),.is_empty(),.equal_to(), and friends. - Keep branch closures pure. Branches return views without side effects. They may run multiple times as conditions toggle.
- Prefer
whenover Rustif/elsein view bodies. Rust’sifevaluates once at construction time;whenupdates as conditions change. - Switch to
.anyview()when branches diverge. Once you reach four or more arms, or each arm produces a different concrete view type, amatchplus.anyview()is clearer than a longwhenchain.
Quick reference
| Pattern | Purpose |
|---|---|
when(cond, || view) | Show view when condition is true |
when(cond, || v).otherwise(|| w) | If/else |
when(a, || v).or(b, || w).otherwise(|| x) | If/else-if/else |
when(!binding, || view) | Show when binding is false |
when(sig.equal_to(val), || view) | Compare signal to value |
when(sig.map(|v| ...), || view) | Derived boolean condition |
match value { Mode::A => a().anyview(), ... } | Multi-branch over an enum |
You now have the tools to build dynamic, condition-driven interfaces. The final piece of the UI puzzle is navigation — how do you move between screens, manage a navigation stack, and organize your app with tabs? That is exactly what the next chapter covers.