共享状态
通常,多个组件需要访问相同的状态。根据你的需求,有几种方法可以实现这一点。
提升状态(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()) }
}
}
}
有时,一些状态需要在树的深处的多个组件之间共享,通过属性传递非常不方便。
假设现在我们想要为我们的应用程序实现一个暗色模式切换。为了实现这一点,我们将使每个组件根据是否启用了暗色模式来选择样式。
注意:我们选择这种方法是为了示例。实现暗色模式有更好的方法(例如,使用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;
}
}
}
}
}