使用 Action 修改数据
Mutating Data with Actions
我们已经讨论了如何使用资源(Resource)加载 async 数据。资源会立即加载数据,并与 <Suspense/> 和 <Transition/> 组件紧密配合,以显示应用中数据的加载状态。但是,如果你只想调用某个任意的 async 函数并跟踪其执行情况,该怎么办呢?
We’ve talked about how to load async data with resources. Resources immediately load data and work closely with <Suspense/> and <Transition/> components to show whether data is loading in your app. But what if you just want to call some arbitrary async function and keep track of what it’s doing?
嗯,你总是可以使用 spawn_local。这允许你通过将 Future 交给浏览器(或者在服务器上交给 Tokio 或你正在使用的其他运行时),在同步环境中派生(spawn)一个 async 任务。但是你如何知道它是否仍在挂起(pending)呢?好吧,你可以设置一个信号(Signal)来显示它是否正在加载,再设置另一个信号来显示结果……
Well, you could always use spawn_local. This allows you to just spawn an async task in a synchronous environment by handing the Future off to the browser (or, on the server, Tokio or whatever other runtime you’re using). But how do you know if it’s still pending? Well, you could just set a signal to show whether it’s loading, and another one to show the result...
这些都可以做到。或者你可以使用最后一个 async 原语:Action。
All of this is true. Or you could use the final async primitive: Action.
Action 和资源看起来很相似,但它们代表了根本不同的东西。如果你试图通过运行 async 函数来加载数据(无论是加载一次还是在其他值变化时加载),你可能想要使用资源。如果你试图偶尔运行一个 async 函数来响应某些操作(例如用户点击按钮),你可能想要使用 Action。
Actions and resources seem similar, but they represent fundamentally different things. If you’re trying to load data by running an async function, either once or when some other value changes, you probably want to use a resource. If you’re trying to occasionally run an async function in response to something like a user clicking a button, you probably want to use an Action.
假设我们有一个想要运行的 async 函数。
Say we have some async function we want to run.
async fn add_todo_request(new_title: &str) -> Uuid {
/* 在服务器上执行一些操作以添加新的待办事项 */
/* do some stuff on the server to add a new todo */
}
Action::new() 接受一个 async 函数,该函数接受对单个参数的引用,你可以将其视为它的“输入类型”。
Action::new() takes an async function that takes a reference to a single argument, which you could think of as its “input type.”
输入始终是单一类型。如果你想传入多个参数,可以使用结构体或元组。
The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple.
// 如果只有一个参数,直接使用它即可 // if there's a single argument, just use that let action1 = Action::new(|input: &String| { let input = input.clone(); async move { todo!() } }); // 如果没有参数,使用单元类型 `()` // if there are no arguments, use the unit type `()` let action2 = Action::new(|input: &()| async { todo!() }); // 如果有多个参数,使用元组 // if there are multiple arguments, use a tuple let action3 = Action::new( |input: &(usize, String)| async { todo!() } );因为 Action 函数接受引用,但
Future需要具有'static生命周期,所以通常需要克隆该值以将其传递到Future中。诚然这有点笨拙,但它开启了一些强大的功能,比如乐观 UI(optimistic UI)。我们将在以后的章节中对此进行更多介绍。Because the action function takes a reference but the
Futureneeds to have a'staticlifetime, you’ll usually need to clone the value to pass it into theFuture. This is admittedly awkward but it unlocks some powerful features like optimistic UI. We’ll see a little more about that in future chapters.
所以在这种情况下,我们创建 Action 所需要做的就是
So in this case, all we need to do to create an action is
let add_todo_action = Action::new(|input: &String| {
let input = input.to_owned();
async move { add_todo_request(&input).await }
});
我们不会直接调用 add_todo_action,而是通过 .dispatch() 调用它,如下所示
Rather than calling add_todo_action directly, we’ll call it with .dispatch(), as in
add_todo_action.dispatch("Some value".to_string());
你可以从事件监听器、超时或任何地方执行此操作;因为 .dispatch() 不是 async 函数,所以它可以从同步上下文中调用。
You can do this from an event listener, a timeout, or anywhere; because .dispatch() isn’t an async function, it can be called from a synchronous context.
Action 提供了对几个信号的访问,这些信号在正在调用的异步 Action 和同步响应式系统之间进行同步:
Actions provide access to a few signals that synchronize between the asynchronous action you’re calling and the synchronous reactive system:
let submitted = add_todo_action.input(); // RwSignal<Option<String>>
let pending = add_todo_action.pending(); // ReadSignal<bool>
let todo_id = add_todo_action.value(); // RwSignal<Option<Uuid>>
这使得跟踪请求的当前状态、显示加载指示器或基于提交将成功的假设执行“乐观 UI”变得容易。
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
let input_ref = NodeRef::<Input>::new();
view! {
<form
on:submit=move |ev| {
ev.prevent_default(); // 阻止页面重新加载...
// don't reload the page...
let input = input_ref.get().expect("input to exist");
add_todo_action.dispatch(input.value());
}
>
<label>
"What do you need to do?"
<input type="text"
node_ref=input_ref
/>
</label>
<button type="submit">"Add Todo"</button>
</form>
// 使用我们的加载状态
// use our loading state
<p>{move || pending.get().then_some("Loading...")}</p>
}
现在,这一切可能看起来有点过于复杂,或者限制太多。我想在这里把 Action 与资源并列介绍,作为拼图中缺失的一块。在一个真实的 Leptos 应用中,实际上你最常将 Action 与服务器函数(Server Function)、ServerAction 以及 <ActionForm/> 组件结合使用,以创建功能强大的渐进式增强表单。所以,如果这个原语对你来说似乎没用……别担心!也许以后它就有意义了。(或者现在就去查看我们的 todo_app_sqlite 示例。)
Now, there’s a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, you’ll actually most often use actions alongside server functions, ServerAction, and the <ActionForm/> component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Don’t worry! Maybe it will make sense later. (Or check out our todo_app_sqlite example now.)
CodeSandbox 源码 (CodeSandbox Source)
use gloo_timers::future::TimeoutFuture;
use leptos::{html::Input, prelude::*};
use uuid::Uuid;
// 在这里我们定义一个异步函数。
// 这可以是任何东西:网络请求、数据库读取等。
// 把它看作是一个变更(mutation):你运行的一些命令式异步操作,
// 而资源将是你加载的一些异步数据。
// Here we define an async function
// This could be anything: a network request, database read, etc.
// Think of it as a mutation: some imperative async action you run,
// whereas a resource would be some async data you load
async fn add_todo(text: &str) -> Uuid {
_ = text;
// 模拟一秒延迟
// fake a one-second delay
// SendWrapper 允许我们使用这个 !Send 浏览器 API;别担心。
// SendWrapper allows us to use this !Send browser API; don't worry about it
send_wrapper::SendWrapper::new(TimeoutFuture::new(1_000)).await;
// 假设这是一个帖子 ID 之类的东西
// pretend this is a post ID or something
Uuid::new_v4()
}
#[component]
pub fn App() -> impl IntoView {
// 一个 Action 接受一个带有单个参数的异步函数。
// 它可以是简单类型、结构体或 ()。
// an action takes an async function with single argument
// it can be a simple type, a struct, or ()
let add_todo = Action::new(|input: &String| {
// 输入是一个引用,但我们需要 Future 拥有它。
// 这很重要:我们需要克隆并移动到 Future 中,
// 这样它就具有了 'static 生命周期。
// the input is a reference, but we need the Future to own it
// this is important: we need to clone and move into the Future
// so it has a 'static lifetime
let input = input.to_owned();
async move { add_todo(&input).await }
});
// Action 提供了一组同步的响应式变量,
// 它们告诉我们关于 Action 状态的不同信息。
// actions provide a bunch of synchronous, reactive variables
// that tell us different things about the state of the action
let submitted = add_todo.input();
let pending = add_todo.pending();
let todo_id = add_todo.value();
let input_ref = NodeRef::<Input>::new();
view! {
<form
on:submit=move |ev| {
ev.prevent_default(); // 阻止页面重新加载...
// don't reload the page...
let input = input_ref.get().expect("input to exist");
add_todo.dispatch(input.value());
}
>
<label>
"What do you need to do?"
<input type="text"
node_ref=input_ref
/>
</label>
<button type="submit">"Add Todo"</button>
</form>
<p>{move || pending.get().then_some("Loading...")}</p>
<p>
"Submitted: "
<code>{move || format!("{:#?}", submitted.get())}</code>
</p>
<p>
"Pending: "
<code>{move || format!("{:#?}", pending.get())}</code>
</p>
<p>
"Todo ID: "
<code>{move || format!("{:#?}", todo_id.get())}</code>
</p>
}
}
fn main() {
leptos::mount::mount_to_body(App)
}