全局状态管理

到目前为止,我们只处理了组件中的局部状态,并学习了如何在父组件和子组件之间协调状态。在某些情况下,人们会寻找一种更通用的全局状态管理解决方案,可以在整个应用程序中使用。

一般来说,你并不需要本章内容。典型的模式是将应用程序分解为组件,每个组件管理自己的局部状态,而不是将所有状态存储在全局结构中。然而,在某些场景下(比如主题管理、保存用户设置或在 UI 的不同部分之间共享数据),你可能需要某种全局状态管理方法。

管理全局状态的三种最佳方法是:

  1. 使用路由器通过 URL 驱动全局状态
  2. 通过上下文传递信号
  3. 使用存储(stores)创建全局状态结构

选项 1:将 URL 作为全局状态

从某种意义上说,URL 实际上是存储全局状态的最佳方式之一。它可以从树中的任何组件访问。还有原生的 HTML 元素(例如 <form><a>)专门用于更新 URL。此外,URL 状态可以跨页面刷新和设备之间保持一致。你可以将 URL 分享给朋友,或从手机发送到笔记本电脑,URL 中存储的任何状态都将被复制。

本教程的接下来的部分会详细介绍路由器相关的主题,因此我们会深入讨论这些内容。

现在,我们只讨论选项 2 和选项 3。


选项 2:通过上下文传递信号

父子通信 部分中,我们看到可以使用 provide_context 将信号从父组件传递到子组件,并使用 use_context 在子组件中读取该信号。而 provide_context 的作用范围不受距离限制。如果你想创建一个可以在应用程序任意位置访问的全局信号,可以使用上下文提供它并通过上下文读取。

通过上下文提供的信号只会在读取的地方引发响应式更新,而不会影响中间的任何组件,因此即使跨组件传递信号,也能保持精细粒度的响应式更新能力。

我们从在应用程序根组件中创建信号开始,然后通过 provide_context 提供该信号,让其可被所有子组件和后代组件访问。

#[component]
fn App() -> impl IntoView {
    // 在根组件中创建信号,可在应用的任何地方使用
    let (count, set_count) = signal(0);
    // 我们会将 setter 传递给特定的组件,
    // 但通过上下文将 count 提供给整个应用
    provide_context(count);

    view! {
        // SetterButton 组件可以修改 count
        <SetterButton set_count/>
        // 以下组件只读该信号
        // 如果需要,也可以通过传递 `set_count` 给予写入权限
        <FancyMath/>
        <ListItems/>
    }
}

<SetterButton/> 是我们之前多次编写的计数器组件类型(请参阅下面的沙箱示例以了解更多)。

<FancyMath/><ListItems/> 都会通过 use_context 消费我们通过上下文提供的信号,并对其执行某些操作。

/// 一个使用全局计数进行“复杂”数学运算的组件
#[component]
fn FancyMath() -> impl IntoView {
    // 通过 `use_context` 消费全局信号
    let count = use_context::<ReadSignal<u32>>()
        // 我们知道刚刚在父组件中提供了这个信号
        .expect("需要有一个 `count` 信号被提供");
    let is_even = move || count.get() & 1 == 0;

    view! {
        <div class="consumer blue">
            "数字 "
            <strong>{count}</strong>
            {move || if is_even() {
                " 是"
            } else {
                " 不是"
            }}
            " 偶数。"
        </div>
    }
}

选项 3:创建全局状态存储(Store)

部分内容与关于复杂迭代中存储的章节 此处 重叠。由于这两部分都是中级/可选内容,因此一些重复不会有太大影响。

存储是 Leptos 0.7 中提供的新型响应式原语,通过伴随的 reactive_stores crate 提供。(目前该 crate 独立发布,以便可以在不影响整个框架版本的情况下继续开发。)

存储允许你包装整个结构体,并对个别字段进行响应式读取和更新,而不会跟踪其他字段的更改。

你可以通过在结构体上添加 #[derive(Store)] 来使用存储。(导入宏时需要使用 use reactive_stores::Store;。)这会为结构体创建一个扩展 trait,当结构体被包裹在 Store<_> 中时,该 trait 提供每个字段的 getter 方法。

#[derive(Clone, Debug, Default, Store)]
struct GlobalState {
    count: i32,
    name: String,
}

这会创建一个名为 GlobalStateStoreFields 的 trait,为 Store<GlobalState> 添加 countname 方法。每个方法返回一个响应式存储字段。

#[component]
fn App() -> impl IntoView {
    provide_context(Store::new(GlobalState::default()));

    // 等等
}

/// 一个更新全局状态中计数的组件
#[component]
fn GlobalStateCounter() -> impl IntoView {
    let state = expect_context::<Store<GlobalState>>();

    // 这让我们能响应式访问 `count` 字段
    let count = state.count();

    view! {
        <div class="consumer blue">
            <button
                on:click=move |_| {
                    *count.write() += 1;
                }
            >
                "增加全局计数"
            </button>
            <br/>
            <span>"计数为: " {move || count.get()}</span>
        </div>
    }
}

点击按钮时只会更新 state.count。如果我们在其他地方读取了 state.name,点击按钮不会通知它。这让你能够结合自上而下的数据流和精细粒度的响应式更新的优势。

查看代码库中的 stores 示例,了解更详细的示例内容。