Dioxus框架中动态渲染(Dynamic Rendering)

367 阅读5分钟

动态渲染

有时,你希望根据状态/属性渲染不同的内容。在Dioxus中,只需使用Rust控制流描述你想要看到的内容——如果状态或属性发生变化,框架将负责即时进行必要的更改!

条件渲染

要根据条件渲染不同的元素,可以使用if-else语句:

if is_logged_in {
    rsx! {
        "Welcome!"
        button { onclick: move |_| log_out.call(()), "Log Out" }
    }
} else {
    rsx! { button { onclick: move |_| log_in.call(()), "Log In" } }
}

你也可以使用match语句或任何Rust函数来有条件地渲染不同的内容。

改进if-else示例

你可能注意到了上面的if-else示例中的一些重复代码。像这样的重复代码既不利于可维护性,也不利于性能。Dioxus将跳过diff静态元素,如按钮,但在多个rsx调用之间切换时无法执行此优化。对于这个示例,两种方法都可以,但对于在条件之间重用大部分内容的组件,可能会有更大的问题。

我们可以通过拆分动态部分并在需要的地方插入它们来改进这个示例。

rsx! {
    // 我们只有在登录时才渲染欢迎消息
    // 你可以在渲染块中间使用if语句来有条件地渲染元素
    if is_logged_in {
        // 注意这个if语句的主体是rsx代码,而不是一个表达式
        "Welcome!"
    }
    button {
        // 根据`is_logged_in`的值,我们将调用不同的事件处理器
        onclick: move |_| if is_logged_in { log_out.call(()) } else { log_in.call(()) },
        if is_logged_in {
            // 如果我们已登录,按钮应该显示"Log Out"
            "Log Out"
        } else {
            // 如果我们未登录,按钮应该显示"Log In"
            "Log In"
        }
    }
}

检查Element属性

由于ElementOption<VNode>,接受Element作为属性的组件可以检查其内容,并根据此渲染不同的内容。示例:

fn Clickable(props: ClickableProps) -> Element {
    match props.children {
        Some(VNode { .. }) => {
            todo!("render some stuff")
        }
        _ => {
            todo!("render some other stuff")
        }
    }
}

你不能修改Element,但如果你需要它的修改版本,可以根据其属性/子元素等构建一个新的。

渲染空内容

要渲染空内容,你可以从组件返回None。这在你想有条件地隐藏某些内容时很有用:

if is_logged_in {
    return None;
}

rsx! { p { "You must be logged in to comment" } }

已登录

你必须登录才能评论

这是因为Element类型只是Option<VNode>的别名。

同样,你可以使用不同的方法有条件地返回None。例如,可以使用布尔值的then()函数。

渲染列表

通常,你想要渲染一系列组件。例如,你可能想要渲染帖子上的所有评论列表。

为此,Dioxus接受产生Element的迭代器。所以我们需要:

  • 获取我们所有项的迭代器(例如,如果你有一个Vec的评论,使用iter()迭代它)
  • 使用.map将迭代器中的每个项转换为LazyNode,使用rsx!{...}
    • 为每个迭代器项添加一个唯一的key属性
  • 在最终的RSX中包含这个迭代器(或将其内联使用)

示例:假设你有一系列评论想要渲染。然后,你可以像这样渲染它们:

let mut comment_field = use_signal(String::new);
let mut next_id = use_signal(|| 0);
let mut comments = use_signal(Vec::<Comment>::new);

let comments_lock = comments.read();
let comments_rendered = comments_lock.iter().map(|comment| {
    rsx! { CommentComponent { comment: comment.clone() } }
});

rsx! {
    form {
        onsubmit: move |_| {
            comments
                .write()
                .push(Comment {
                    content: comment_field(),
                    id: next_id(),
                });
            next_id += 1;
            comment_field.set(String::new());
        },
        input {
            value: "{comment_field}",
            oninput: move |event| comment_field.set(event.value())
        }
        input { r#type: "submit" }
    }
    {comments_rendered}
}

内联for循环

由于渲染项目列表非常常见,Dioxus为此提供了简写。而不是使用.iter.maprsx,你可以使用带有rsx代码主体的for循环:

let mut comment_field = use_signal(String::new);
let mut next_id = use_signal(|| 0);
let mut comments = use_signal(Vec::<Comment>::new);

rsx! {
    form {
        onsubmit: move |_| {
            comments
                .write()
                .push(Comment {
                    content: comment_field(),
                    id: next_id(),
                });
            next_id += 1;
            comment_field.set(String::new());
        },
        input {
            value: "{comment_field}",
            oninput: move |event| comment_field.set(event.value())
        }
        input { r#type: "submit" }
    }
    for comment in comments() {
        // 注意这个for循环的主体是rsx代码,而不是一个表达式
        CommentComponent { comment }
    }
}

key属性

每次你重新渲染列表时,Dioxus需要跟踪哪些项去哪里,以确定需要对UI进行哪些更新。

例如,假设CommentComponent有一些状态——例如,用户在其中输入回复的字段。如果评论的顺序突然改变,Dioxus需要正确地将该状态与相同的评论关联起来——否则,用户最终可能会回复到不同的评论!

为了帮助Dioxus跟踪列表项,我们需要将每个项与一个唯一的键关联。在上面的示例中,我们动态生成了唯一的键。在实际应用中,键更有可能来自例如数据库ID。只要你满足以下要求,从哪里获取键并不重要:

  • 键在列表中必须是唯一的
  • 同一个项应该总是与同一个键关联
  • 键应该相对较小(即,将整个Comment结构转换为字符串将是一个相当糟糕的键),以便可以高效地进行比较

你可能会倾向于使用列表中项的索引作为其键。如果你没有指定键,Dioxus将使用它。只有在你能确保列表是恒定的情况下,即没有重新排序、添加或删除,这才是可以接受的。

注意,如果你将键传递给你制作的组件,它不会将键作为属性接收。它只被Dioxus本身用作提示。如果你的组件需要一个ID,你必须将其作为单独的属性传递。

img