状态管理

131 阅读9分钟

状态管理

在 Dioxus 中,您的 app 被定义为用于状态的函数。当状态发生变化时,应用程序中依赖于该状态的部分将自动重新运行。其响应式机制会自动跟踪状态的变化, 并更新程序中的并由他派生的状态。

创建状态

在 Dioxus 中, 您可以使用 Signal 创建可变状态。信号是一种被跟踪的值,当您更改它们时,它们会自动更新您的应用程序。它们构成了应用程序 状态 的骨架,您可以从中衍生出其他状态。Signal 通常通过事件处理程序或异步任务由用户输入直接驱动。

您可以使用 use_signal 钩子来创建 1 个信号.

let mut signal = use_signal(|| 0);

一旦有了信号,就可以像调用函数一样调用信号来克隆它,或者使用方法 .read() 方法来获得其内部值的一个引用:

// 使用函数调用的方式能获得当前值的克隆值
let value: i32 = signal();
// 通过 .read() 方法能获得其内部值的1个引用
let value: &i32 = &signal.read();
// 或者使用其他 traits 比如下面的 Display 字符串格式化
log!("{signal}");

最后, 你还可以通过 .set() 来直接设置值, 或者使用 .write() 获得他内部值的可变引用:

// 直接从 signal 设置值
signal.set(1);
// 使用 .write() 获得其内部值的可变引用
let mut value: &mut i32 = &mut signal.write();
// 然后通过其引用进行更新
*value += 1;

响应式作用域

Dioxus 中最基础的响应式方式是使用 use_effect 钩子. 他会创建 1 个闭包, 每次闭包内的跟踪值改变时, 该闭包都会被重新运行. 闭包内任意读取的值都会变成该 effect 的依赖.如果值被改变, 该 effect 会被重新运行.

fn Effect() -> Element {
    // 使用 use_signal 创建1个可跟踪值
    let mut count = use_signal(|| 0);

    use_effect(move || {
        // 当我们读取这个变量时, 他就变成了这个 effect 的依赖
        let current_count = count();
        // 无论什么时候这个变量值被更新, effect 都会重新运行
        log!("{current_count}");
    });

    rsx! {
        button { onclick: move |_| count += 1, "Increment" }

        div { "Count is {count}" }
    }
}

状态的派生

use_memo 也是响应式的, 他能从任何被跟踪的值派生1个新的状态. 它需要1个闭包参数, 该闭包通过计算新的状态, 返回当前 memo 状态的1个跟踪值.每当 memo 的依赖改变值时, 该闭包都会重新执行. 闭包返回的值只有在闭包输出改变(PartialEq 比较新旧值)时才改变.

fn Memo() -> Element {
    let mut count = use_signal(|| 0);

	// use_memo 从 count 派生了1个新的跟踪值
	// 当在闭包内读取 signal 时, 它就变成了 memo 的依赖
	// 无论合适其值 count 变好时, 该 memo 都会重新运行
    let half_count = use_memo(move || count() / 2);

    use_effect(move || {
		// half_count 是派生自 count signal 的跟踪值
		// 当我们在闭包内读取 half_count 的值时, 它就变成了 effect 的依赖, 所以当 half_count 改变时, 该 effect 将重新运行
		// 但是由于是使用 memo 派生的,所以当 count 改变时 memo 重新运行
		// 如果其 memo 返回的值没有变化, 这边的 effect 不会重新运行
        log!("{half_count}");
    });

    rsx! {
        button { onclick: move |_| count += 1, "Increment" }

        div { "Count is {count}" }
        div { "Half count is {half_count}" }
    }
}

派生异步状态

use_resource 也是响应式的, 它可以从任意 异步闭包派生.它需要一个异步闭包, 计算新的状态, 并返回当前 resource 状态的跟踪值.任何时候resource 的依赖改变时, 它都会被重新运行. 闭包返回的值只有在其 future 改变时才会改变, 和 use_memo 不同的是 resource 的输出结果不会进行 PartialEq 比较.

fn Resource() -> Element {
    let mut count = use_signal(|| 0);

	// use_resource 从 count 派生了一个跟踪值
	// 当我们在闭包内读取 count, 代表它变成了 resource 的1个依赖
	// 只要 count 的值一变化, 就会触发 resource 的重新运行
    let half_count = use_resource(move || async move {
        // You can do async work inside resources
        gloo_timers::future::TimeoutFuture::new(100).await;
        count() / 2
    });

    use_effect(move || {
		// half_count 作为该 effect 的依赖, 其值变化时该effect 都会重新执行
        log!("{:?}", half_count());
    });

    rsx! {
        button { onclick: move |_| count += 1, "Change Signal" }

        div { "Count is {count}" }
        div { "Half count is {half_count():?}" }
    }
}

派生 UI

组件是一个返回 UI 的函数.他们会记忆函数的输出, 就像 memo 一样.组件会保持追踪组件内读取的依赖, 并在他们改变时重新运行.

fn Component() -> Element {
    let mut count = use_signal(|| 0);

    rsx! {
        button { onclick: move |_| count += 1, "Change Signal" }
		// 读取 count 的行为, 让它变成了该组件的依赖, 当其值变化时, 该组件就会重新运行
        Count { count: count() }
    }
}

// 组件自动记忆他们的 props 属性, 如果属性变化, 该组件 (Count) 也会重新运行
#[component]
fn Count(count: i32) -> Element {
    rsx! {
        div { "Count: {count}" }
    }
}

处理未被追踪的状态

App 中的大多数情况都是拥有追踪值的. Dioxus 所有内置的钩子都返回跟踪的值,我们鼓励自定义 hooks 执行相同的操作. 但是,有时您需要使用未跟踪的状态。例如,您可能会收到原始的未跟踪值。当您在反应性上下文中读取未跟踪的值时,它不会订阅该值:

fn Component() -> Element {
    let mut count = use_signal(|| 0);

    rsx! {
        button { onclick: move |_| count += 1, "Change Signal" }

        Count { count: count() }
    }
}

// 代码中的 count 参数变化时, 该组件会重新运行
// 但是需要注意的是它并不是一个追踪值
#[component]
fn Count(count: i32) -> Element {
	// 所以当你在 memo 中读取该值时, 他没有订阅 count 的信号, 因为其值并不是追踪值,是不支持响应式的
    let double_count = use_memo(move || count * 2);

    rsx! {
        div { "Double count: {double_count}" }
    }
}

如果运行代码, 你会发现, 每次点击都会有日志输出 (说明count的变化触发了组件的重新运行). 但是div 中展示的文本没有变化(注意 其中使用的是 double_count 来自 memo).

你可以通过使用 use_reactive hook 来重新追踪原始状态.该 hook 的闭包需要一个元组参数作为依赖, 然后返回一个响应式的闭包.当闭包在一个响应式的上下文中被调用时, 他将追踪/订阅依赖,并在依赖改变时,重新运行该闭包.

#[component]
fn Count(count: i32) -> Element {
	// 你可以使用 use_reactive hook 来手动追踪非响应式的值.
    let double_count = use_memo(
		// 使用 reactive 来声明一个元组的参数作为依赖, 然后返回一个响应式的闭包
        use_reactive!(|(count,)| count * 2),
    );

    rsx! {
        div { "Double count: {double_count}" }
    }
}

响应式的 Props

为了确保 props 拥有响应式, 我们推荐你使用 ReadOnlySignal 来保护你的 props. Dioxus会在传递参数给组件时, 自动将其 T 类型转换为 ReadOnlySignal<T> 的参数类型.这将确保你的 props 是被追踪的, 从而确保其值改变时, 该组件可以得到正确的响应.

// 你可以使用 ReadOnlySignal 定义你的 props 类型.
// 在组件的 T 参数被传递时, Dioxus 将自动转为 ReadOnlySignal<T> 类型
#[component]
fn Count(count: ReadOnlySignal<i32>) -> Element {
	// 当你在 memo 中读取值时, 他将订阅 count 的信号
    let double_count = use_memo(move || count() * 2);

    rsx! {
        div { "Double count: {double_count}" }
    }
}

状态的传递

当您在应用程序中创建信号和派生状态时,您需要在组件之间移动该状态。 Dioxus提供了三种不同的方式来传递状态

传递属性 props

你可以通过组件的参数来传递值.这应该是你默认的使用方式.这是明确的, 组件本地的传递方式. 尽量使用这种方式,除非拥有了过多烦人的参数.

pub fn ParentComponent() -> Element {
    let count = use_signal(|| 0);

    rsx! {
        "Count is {count}"
        IncrementButton {
            count
        }
    }
}

#[component]
fn IncrementButton(mut count: Signal<i32>) -> Element {
    rsx! {
        button {
            onclick: move |_| count += 1,
            "Increment"
        }
    }
}

上下文传递

如果你需要一个更强大的方式传递状态, 可以使用 上下文的 API.

上下文运行你从父组件传递给所有的子组件.如果需要在多个组件之间共享状态,这是很有用的一种方式.你可以使用 use_context_provider hook 在父组件中插入一个唯一类型 (unique type)的值到上下文.那么你就可以在其子组件中使用 use_context hook 来访问上下文中的该值.

#[derive(Clone, Copy)]
struct MyState {
    count: Signal<i32>,
}

pub fn ParentComponent() -> Element {
	// 使用 use_context_provider 来驱动
	// 其(唯一类型的)值供所有子组件访问
    let state = use_context_provider(|| MyState {
        count: Signal::new(0),
    });

    rsx! {
        "Count is {state.count}"
		// 现在即使没有明确传递属性给 IncrementButton 组件
		// 它也可以从上下文访问该 count 属性
        IncrementButton {}
    }
}

#[component]
fn IncrementButton() -> Element {
	// 使用 上下文从父组件获取其值
	// 注意这边使用的是 ::<UniqueType> 所有我们在之前注入的时候需要确保类型唯一, 不然后面就无法正确获取需要的类型
    let mut count = use_context::<MyState>().count;

    rsx! {
        button {
            onclick: move |_| count += 1,
            "Increment"
        }
    }
}

这种方式虽然不是很明确的看出属性的传递, 但他仍然是针对组件层级的(local).如果你该状态你需要在你app的部分范围共享,这是一个很好的选择. 它使您可以创建多个全球状态,同时在重复使用组件时仍使状态不同。如果我创建一个新的 ParentComponent,它将有一个新的 MyState

使用 globals

最后, 如果你需要一个真的全局状态, 你可以使用静态的Global<T> 来定义你的状态.如果你需要在全局 app 共享状态,这是很有用的.

use dioxus::prelude::*;

// Globals 类型, 会在你第一次访问其值时通过闭包创建
static COUNT: GlobalSignal<i32> = Global::new(|| 0);

pub fn ParentComponent() -> Element {
    rsx! {
        "Count is {COUNT}"
        IncrementButton {}
    }
}

fn IncrementButton() -> Element {
    rsx! {
        button {
            // You don't need to pass anything around or get anything out of the context because COUNT is global
            onclick: move |_| *COUNT.write() += 1,
            "Increment"
        }
    }
}

如果您的状态确实需要全局,那么全球状态也是非常符合人体工程学,但是如果状态在组件的不同实例下是不同的表现,则不应使用它。如果我创建另一个IncrementButton ,它将使用相同的 COUNT。通常对于一个库来说应避免这种情况,以使组件更具有复用性。

注意 即使 COUNT 是 静态的, 但是它对于每个 app 实例来说也是不同的, 所以即使你有多个app实例在服务区运行, 也不需要担心状态的管理问题.