Dioxus框架中共享状态(Context)

270 阅读4分钟

共享状态

通常,多个组件需要访问相同的状态。根据你的需求,有几种方法可以实现这一点。

提升状态(Lifting State)

在组件之间共享状态的一种方法是将状态“提升”到最近的共同祖先。这意味着将use_signal钩子放在父组件中,并将所需的值作为属性向下传递。

假设我们想要构建一个表情包编辑器。我们希望有一个输入框来编辑表情包的标题,但同时也希望有一个带有标题的表情包预览。从逻辑上讲,表情包和输入框是两个独立的组件,但它们需要访问相同的状态(当前标题)。

当然,在这个简单的例子中,我们可以将所有内容写在一个组件中——但将所有内容拆分成更小的组件,可以使代码更可重用、更易于维护,并且性能更好(对于更大、更复杂的应用程序来说,这一点尤其重要)。

我们从一个负责渲染给定标题的表情包的Meme组件开始:

#[component]
fn Meme(caption: String) -> Element {
    let container_style = r#"
        position: relative;
        width: fit-content;
    "#;

    let caption_container_style = r#"
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        padding: 16px 8px;
    "#;

    let caption_style = r"
        font-size: 32px;
        margin: 0;
        color: white;
        text-align: center;
    ";

    rsx! {
        div { style: "{container_style}",
            img { src: "https://i.imgflip.com/2zh47r.jpg", height: "500px" }
            div { style: "{caption_container_style}", p { style: "{caption_style}", "{caption}" } }
        }
    }
}

注意Meme组件不知道标题来自哪里——它可能存储在use_signal中,或者是一个常量。这确保了它的可重用性非常高——相同的组件可以用于表情包画廊,无需任何更改!

我们还创建了一个与表情包完全解耦的标题编辑器。标题编辑器不应存储标题本身——否则,我们如何将其提供给Meme组件呢?相反,它应该接受当前标题作为属性,以及一个事件处理器来委托输入事件:

#[component]
fn CaptionEditor(caption: String, oninput: EventHandler<FormEvent>) -> Element {
    let input_style = r"
        border: none;
        background: cornflowerblue;
        padding: 8px 16px;
        margin: 0;
        border-radius: 4px;
        color: white;
    ";

    rsx! {
        input {
            style: "{input_style}",
            value: "{caption}",
            oninput: move |event| oninput.call(event)
        }
    }
}

最后,第三个组件将渲染其他两个组件作为子组件。它将负责维护状态并传递相关属性。

fn MemeEditor() -> Element {
    let container_style = r"
        display: flex;
        flex-direction: column;
        gap: 16px;
        margin: 0 auto;
        width: fit-content;
    ";

    let mut caption = use_signal(|| "me waiting for my rust code to compile".to_string());

    rsx! {
        div { style: "{container_style}",
            h1 { "Meme Editor" }
            Meme { caption: caption }
            CaptionEditor { caption: caption, oninput: move |event: FormEvent| caption.set(event.value()) }
        }
    }
}

img

有时,一些状态需要在树的深处的多个组件之间共享,通过属性传递非常不方便。

假设现在我们想要为我们的应用程序实现一个暗色模式切换。为了实现这一点,我们将使每个组件根据是否启用了暗色模式来选择样式。

注意:我们选择这种方法是为了示例。实现暗色模式有更好的方法(例如,使用CSS变量)。假设CSS变量不存在——欢迎来到2013年!

现在,我们可以在顶层组件(如App)中再写一个use_signal,并通过属性将is_dark_mode向下传递给每个组件。但想想随着应用程序复杂性的增长会发生什么——几乎所有渲染任何CSS的组件都需要知道是否启用了暗色模式——所以它们都需要相同的暗色模式属性。每个父组件都需要向下传递它。想象一下这会变得多么混乱和冗长,特别是如果我们有多个级别的组件!

Dioxus提供了比“prop drilling”更好的解决方案——提供上下文。use_context_provider钩子为任何子组件提供任何可克隆的上下文(包括Signals!)。子组件可以使用use_context钩子获取该上下文,如果它是一个Signal,它们可以读取和写入它。

首先,我们必须为我们的暗色模式配置创建一个结构体:

#[derive(Clone, Copy)]
struct DarkMode(bool);

现在,在顶层组件(如App)中,我们可以为所有子组件提供DarkMode上下文:

use_context_provider(|| Signal::new(DarkMode(false)));

结果,App的任何子组件(直接或间接)都可以访问DarkMode上下文。

let dark_mode_context = use_context::<Signal<DarkMode>>();

这里use_context返回Signal<DarkMode>,因为Signal是由父组件提供的。如果上下文没有被提供,use_context将panic。

如果你有一个组件,其中上下文可能被提供也可能不被提供,你可能想使用try_consume_context,这样你可以处理None的情况。这种方法的缺点是它不会在渲染之间记忆值,所以它不会像use_context那样高效,但你可以自己使用use_hook来做。

例如,以下是我们如何实现暗色模式切换,它既读取上下文(以确定它应该渲染什么颜色)又写入它(以切换暗色模式):

pub fn DarkModeToggle() -> Element {
    let mut dark_mode = use_context::<Signal<DarkMode>>();

    let style = if dark_mode().0 { "color:white" } else { "" };

    rsx! {
        label { style: "{style}",
            "Dark Mode"
            input {
                r#type: "checkbox",
                oninput: move |event| {
                    let is_enabled = event.value() == "true";
                    dark_mode.write().0 = is_enabled;
                }
            }
        }
    }
}

img