Structure

keyed

Build a branch from a derived value and rebuild it only when that value changes.

FNkeyed
Pages
use crownui::prelude::*;
 
#[derive(Clone, Copy, PartialEq)]
enum Page {
    General,
    Network,
    About,
}
 
fn page_view(page: Signal<Page>) -> impl View {
    keyed(
        move |rt| page.get(rt),
        |_cx, page| match page {
            Page::General => text("General settings").boxed(),
            Page::Network => vstack((text("Network"), divider())).boxed(),
            Page::About => text("CrownOS").font_bold().boxed(),
        },
    )
}
Signature
pub fn keyed<T, V, C, B>(key: C, build: B) -> Element<KeyedKind<C, B>>
where
    T: Clone + PartialEq + 'static,
    C: FnMut(&mut Runtime) -> T + 'static,
    B: FnMut(&mut Cx, T) -> V + 'static,
    V: View,

keyed builds a branch from a derived value and rebuilds it only when that value changes. It is show for any number of branches.

Parameters

keyC: FnMut(&mut Runtime) -> T + 'staticrequired

Derives the value that decides the structure. keyed wraps it in a memo, so the closure re-runs when the signals it reads change, and the branch rebuilds only when the result is not equal to the last one. T must be Clone + PartialEq.

buildB: FnMut(&mut Cx, T) -> V + 'staticrequired

Builds the branch for a key value. It runs once on mount and once per key change.

Returns

Element<KeyedKind<C, B>>Element

A wrapper node that holds the current branch. It defaults to a column and takes every style, event and semantic method.

Behavior

  • The old branch unmounts before the new one builds. Its scope is disposed, so state created inside the branch is lost.
  • build reads the key value untracked, so the branch subscribes only to what its own bindings read.
  • When the branch root is styled display_none, the wrapper collapses and takes no space or gap among its siblings.

Return different types with boxed

Each arm of a match has its own type. Call .boxed() on every arm to turn them into one AnyView.

#[derive(Clone, PartialEq)]
enum Load {
    Loading,
    Empty,
    Ready(Vec<String>),
}
 
keyed(
    move |rt| state.get(rt),
    |_cx, load| match load {
        Load::Loading => text("Loading…").boxed(),
        Load::Empty => text("Nothing here").text_color(rgb(0x64748b)).boxed(),
        Load::Ready(names) => vstack(names.into_iter().map(|name| text(name)).collect::<Vec<_>>())
            .gap(4.0)
            .boxed(),
    },
)

Name only what changes the structure

The key decides when the whole branch is rebuilt. Make it as coarse as you can, and let bindings inside the branch handle finer changes.

Too fine
// Rebuilds the branch on every count change.
keyed(move |rt| count.get(rt), |_cx, count| text(format!("{count} items")))

Use cx in the branch

build receives &mut Cx, so a branch can create state, read signals or register effects. Everything it creates belongs to the branch scope and is freed on the next rebuild.

keyed(
    move |rt| selected.get(rt),
    |cx, id: u32| {
        let draft = cx.signal(String::new());
        vstack((
            text(format!("Item {id}")).font_semibold(),
            text_field(draft, "Add a note"),
        ))
        .gap(8.0)
    },
)

The note field starts empty every time selected changes, because the branch and its draft signal are rebuilt.

Why it works this way

keyed turns a value into structure. The rebuild is limited to one branch, and it runs only when a value you chose changes. Everything outside the branch keeps its nodes and state.

See also