全局状态管理
Global State Management
到目前为止,我们只在组件中使用局部状态,并了解了如何在父子组件之间协调状态。有时,人们会寻求一种更通用的全局状态管理解决方案,以便在整个应用程序中使用。
So far, we've only been working with local state in components, and we’ve seen how to coordinate state between parent and child components. On occasion, there are times when people look for a more general solution for global state management that can work throughout an application.
通常情况下,你并不需要这一章。 典型的模式是由组件组合成应用程序,每个组件管理自己的局部状态,而不是将所有状态存储在一个全局结构中。然而,在某些情况下(如主题设置、保存用户设置或在 UI 不同部分的组件之间共享数据),你可能希望使用某种形式的全局状态管理。
In general, you do not need this chapter. The typical pattern is to compose your application out of components, each of which manages its own local state, not to store all state in a global structure. However, there are some cases (like theming, saving user settings, or sharing data between components in different parts of your UI) in which you may want to use some kind of global state management.
全局状态的三种最佳方法是:
The three best approaches to global state are
- 使用路由通过 URL 驱动全局状态
- Using the router to drive global state via the URL
- 通过上下文(Context)传递信号
- Passing signals through context
- 使用 Store 创建全局状态结构体
- Creating a global state struct using stores
选项 #1:将 URL 作为全局状态
Option #1: URL as Global State
在许多方面,URL 实际上是存储全局状态的最佳方式。它可以从组件树中任何位置的任何组件访问。原生 HTML 元素如 <form> 和 <a> 的存在纯粹是为了更新 URL。而且它在页面重载和设备之间保持持久;你可以与朋友分享 URL,或将其从手机发送到笔记本电脑,其中存储的任何状态都将被复制。
In many ways, the URL is actually the best way to store global state. It can be accessed from any component, anywhere in your tree. There are native HTML elements like <form> and <a> that exist solely to update the URL. And it persists across page reloads and between devices; you can share a URL with a friend or send it from your phone to your laptop and any state stored in it will be replicated.
本教程接下来的几个部分将围绕路由器展开,我们将更深入地探讨这些话题。
The next few sections of the tutorial will be about the router, and we’ll get much more into these topics.
但现在,我们只看选项 #2 和 #3。
But for now, we'll just look at options #2 and #3.
选项 #2:通过上下文传递信号
Option #2: Passing Signals through Context
在关于父子通信的章节中,我们看到可以使用 provide_context 将信号从父组件传递给子组件,并使用 use_context 在子组件中读取它。但是 provide_context 跨越任何距离都有效。如果你想创建一个持有某部分状态的全局信号,你可以在提供它的组件的任何后代中提供并访问它。
In the section on parent-child communication, we saw that you can use provide_context to pass signal from a parent component to a child, and use_context to read it in the child. But provide_context works across any distance. If you want to create a global signal that holds some piece of state, you can provide it and access it via context anywhere in the descendants of the component where you provide it.
通过上下文提供的信号仅在读取它的地方引起响应式更新,而不会在中间的任何组件中引起更新,因此即使在远距离也保持了细粒度响应式更新的能力。
A signal provided via context only causes reactive updates where it is read, not in any of the components in between, so it maintains the power of fine-grained reactive updates, even at a distance.
我们首先在应用程序的根部创建一个信号,并使用 provide_context 将其提供给所有子组件和后代组件。
We start by creating a signal in the root of the app and providing it to
all its children and descendants using provide_context.
#[component]
fn App() -> impl IntoView {
// 这里我们在根部创建一个可以在应用中任何地方消费的信号。
// here we create a signal in the root that can be consumed
// anywhere in the app.
let (count, set_count) = signal(0);
// 我们将把 setter 传递给特定组件,
// 但通过 context 将 count 本身提供给整个应用
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(count);
view! {
// SetterButton 被允许修改 count
// SetterButton is allowed to modify the count
<SetterButton set_count/>
// 这些消费者只能从中读取
// 但如果我们愿意,也可以通过传递 `set_count` 给它们写入权限
// These consumers can only read from it
// But we could give them write access by passing `set_count` if we wanted
<FancyMath/>
<ListItems/>
}
}
<SetterButton/> 是我们已经写过好几次的那种计数器。
<SetterButton/> is the kind of counter we’ve written several times now.
<FancyMath/> 和 <ListItems/> 都通过 use_context 消费我们提供的信号并对其进行操作。
<FancyMath/> and <ListItems/> both consume the signal we’re providing via
use_context and do something with it.
/// 一个使用全局 count 信号进行“花哨”数学运算的组件
/// A component that does some "fancy" math with the global count
#[component]
fn FancyMath() -> impl IntoView {
// 这里我们通过 `use_context` 消费全局 count 信号
// here we consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>()
// 我们知道刚才在父组件中提供了这个信号
// we know we just provided this in the parent component
.expect("there to be a `count` signal provided");
let is_even = move || count.get() & 1 == 0;
view! {
<div class="consumer blue">
"The number "
<strong>{count}</strong>
{move || if is_even() {
" is"
} else {
" is not"
}}
" even."
</div>
}
}
选项 #3:创建全局状态 Store
Option #3: Create a Global State Store
此内容的一部分重复自关于 Store 复杂迭代的章节 此处。由于这两个章节都是中级/可选内容,我认为有些重复没有坏处。
Some of this content is duplicated from the section on complex iteration with stores here. Both sections are intermediate/optional content, so I thought some duplication couldn’t hurt.
Store 是一种新的响应式原语,在 Leptos 0.7 中通过配套的 reactive_stores crate 提供。(这个 crate 目前是单独发布的,这样我们就可以在不要求整个框架变更版本的情况下继续开发它。)
Stores are a new reactive primitive, available in Leptos 0.7 through the accompanying reactive_stores crate. (This crate is shipped separately for now so we can continue to develop it without requiring a version change to the whole framework.)
Store 允许你包装整个结构体,并响应式地读取和更新单个字段,而不会跟踪对其他字段的更改。
Stores allow you to wrap an entire struct, and reactively read from and update individual fields without tracking changes to other fields.
通过在结构体上添加 #[derive(Store)] 来使用它们。(你可以使用 use reactive_stores::Store; 来导入宏。)当结构体被包装在 Store<_> 中时,这将创建一个扩展 trait,为结构体的每个字段提供 getter。
They are used by adding #[derive(Store)] onto a struct. (You can use reactive_stores::Store; to import the macro.) This creates an extension trait with a getter for each field of the struct, when the struct is wrapped in a Store<_>.
#[derive(Clone, Debug, Default, Store)]
struct GlobalState {
count: i32,
name: String,
}
这会创建一个名为 GlobalStateStoreFields 的 trait,它向 Store<GlobalState> 添加了 count 和 name 方法。每个方法返回一个响应式 Store 字段。
This creates a trait named GlobalStateStoreFields which adds with methods count and name to a Store<GlobalState>. Each method returns a reactive store field.
#[component]
fn App() -> impl IntoView {
provide_context(Store::new(GlobalState::default()));
// 等等。
// etc.
}
/// 一个更新全局状态中 count 的组件。
/// A component that updates the count in the global state.
#[component]
fn GlobalStateCounter() -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
// 这仅让我们对 `count` 字段拥有响应式访问权限
// this gives us reactive access to the `count` field only
let count = state.count();
view! {
<div class="consumer blue">
<button
on:click=move |_| {
*count.write() += 1;
}
>
"Increment Global Count"
</button>
<br/>
<span>"Count is: " {move || count.get()}</span>
</div>
}
}
点击此按钮仅更新 state.count。如果我们在其他地方读取 state.name,点击该按钮将不会通知它。这使你能够结合自顶向下数据流和细粒度响应式更新的优点。
Clicking this button only updates state.count. If we read from state.name somewhere else,
click the button won’t notify it. This allows you to combine the benefits of a top-down
data flow and of fine-grained reactive updates.
查看仓库中的 stores 示例以获取更详尽的示例。
Check out the stores example in the repo for a more extensive example.