Dioxus框架中钩子(Hooks)

337 阅读4分钟

钩子和组件状态

到目前为止,我们的组件像普通的Rust函数一样没有状态。然而,在UI组件中,通常需要有状态的功能来构建用户交互。例如,你可能想要跟踪用户是否打开了下拉菜单,并相应地渲染不同的内容。

钩子允许我们在组件中创建状态。钩子是你在组件中以恒定的顺序调用的Rust函数,它们为组件添加额外的功能。

Dioxus提供了许多内置钩子,但如果这些钩子不符合你的特定用例,你也可以创建自己的钩子。

use_signal钩子

use_signal是最简单的钩子之一。

  • 你提供一个闭包来确定初始值:let mut count = use_signal(|| 0);
  • use_signal给你当前值,以及写入值的方法
  • 当值更新时,use_signal会使组件重新渲染(以及任何引用它的其他组件),然后为你提供新值。

例如,你可能已经看到了计数器示例,其中使用use_signal钩子跟踪状态(一个数字):

pub fn App() -> Element {
    // 组件第一次渲染时,count将被初始化为0
    let mut count = use_signal(|| 0);

    rsx! {
        h1 { "High-Five counter: {count}" }
        button { onclick: move |_| count += 1, "Up high!" }
        button { onclick: move |_| count -= 1, "Down low!" }
    }
}

High-Five counter: 0

每次组件的状态发生变化时,它都会重新渲染,并且会调用组件函数,因此你可以描述你希望新的UI是什么样子。你不需要担心“改变”任何东西——根据状态描述你想要的东西,Dioxus会处理其余的事情!

use_signal返回的值被包裹在类型为Signal的智能指针中,它是Copy。这就是为什么你可以在事件处理器中读取值和更新它。

如果需要,你可以在同一个组件中使用多个钩子:

pub fn App() -> Element {
    let mut count_a = use_signal(|| 0);
    let mut count_b = use_signal(|| 0);

    rsx! {
        h1 { "Counter_a: {count_a}" }
        button { onclick: move |_| count_a += 1, "a++" }
        button { onclick: move |_| count_a -= 1, "a--" }
        h1 { "Counter_b: {count_b}" }
        button { onclick: move |_| count_b += 1, "b++" }
        button { onclick: move |_| count_b -= 1, "b--" }
    }
}

Counter_a: 0

Counter_b: 0

你也可以使用use_signal来存储更复杂的状态,如Vec。你可以使用readwrite方法来读取和写入状态:

pub fn App() -> Element {
    let mut list = use_signal(Vec::new);

    rsx! {
        p { "Current list: {list:?}" }
        button {
            onclick: move |event| {
                let list_len = list.len();
                list.push(list_len);
                list.push(list_len);
            },
            "Add two elements!"
        }
    }
}

钩子的规则

上述示例可能看起来有点神奇,因为Rust函数通常不与状态关联。Dioxus允许钩子通过与组件关联的隐藏作用域来跨渲染维护状态。

但是Dioxus如何区分同一组件中的多个钩子呢?正如你在第二个示例中看到的,两个use_signal函数都使用了相同的参数,那么为什么它们在计数器不同的时候可以返回不同的内容呢?

let mut count_a = use_signal(|| 0);
let mut count_b = use_signal(|| 0);

这是可能的,因为两个钩子总是以相同的顺序被调用,所以Dioxus知道哪个是哪个。因为调用钩子的顺序很重要,使用钩子时你必须遵循某些规则:

  1. 钩子只能在组件或其他钩子中使用(我们稍后会讲到这一点)。
  2. 在每次调用组件函数时。
  3. 必须调用相同的钩子(除了在错误处理章节后面解释的早期返回情况)。
  4. 以相同的顺序。
  5. 钩子的名称应该以use_开头,以免你不小心将它们与普通函数混淆(use_signal(), use_signal(), use_resource()等)。

这些规则意味着你不能在某些情况下使用钩子:

不要在条件语句中使用钩子

// ❌ 不要在条件语句中调用钩子!
// 我们必须确保每次都会调用相同的钩子
// 但是`if`语句只在条件为真时运行!
// 所以我们可能会违反规则2。
if you_are_happy && you_know_it {
    let something = use_signal(|| "hands");
    println!("clap your {something}")
}

// ✅ 相反,*总是*调用use_signal
// 你可以在条件语句中放置其他东西
let something = use_signal(|| "hands");
if you_are_happy && you_know_it {
    println!("clap your {something}")
}

不要在闭包中使用钩子

// ❌ 不要在闭包中调用钩子!
// 我们不能保证闭包(如果使用)每次都会以相同的顺序被调用
let _a = || {
    let b = use_signal(|| 0);
    b()
};

// ✅ 相反,将钩子`b`移到外部
let b = use_signal(|| 0);
let _a = || b();

不要在循环中使用钩子

// `names`是一个Vec<&str>

// ❌ 不要在循环中使用钩子!
// 在这种情况下,如果Vec的长度发生变化,我们就违反了规则2
for _name in &names {
    let is_selected = use_signal(|| false);
    println!("selected: {is_selected}");
}

// ✅ 相反,使用带有use_signal的hashmap
let selection_map = use_signal(HashMap::<&str, bool>::new);

for name in &names {
    let is_selected = selection_map.read()[name];
    println!("selected: {is_selected}");
}

额外资源

  • dioxus_hooks API文档
  • dioxus_hooks源代码

img