使用 Action 修改数据
我们已经讨论了如何使用 resource 加载 async
数据。Resource 会立即加载数据,并与 <Suspense/>
和 <Transition/>
组件紧密合作,以显示你的应用程序中是否正在加载数据。但是,如果你只是想调用一些任意的 async
函数并跟踪它的执行情况,该怎么办?
好吧,你总是可以使用 spawn_local
。这允许你通过将 Future
交给浏览器(或者在服务器上,是 Tokio 或任何你正在使用的其他运行时)来在同步环境中生成一个 async
任务。但是你如何知道它是否仍在等待中?好吧,你可以设置一个信号来显示它是否正在加载,另一个信号来显示结果...
所有这些都是真的。或者你可以使用最终的 async
原语:create_action
。
Action 和 resource 看起来很相似,但它们代表着根本不同的东西。如果你试图通过运行一个 async
函数来加载数据,无论是运行一次还是在其他值发生变化时运行,你可能想使用 create_resource
。如果你试图偶尔运行一个 async
函数来响应用户点击按钮之类的事情,你可能想使用 create_action
。
假设我们有一些想要运行的 async
函数。
async fn add_todo_request(new_title: &str) -> Uuid {
/* 在服务器上做一些添加新的待办事项的事情 */
}
create_action
接受一个 async
函数,该函数接受对单个参数的引用,你可以将其视为其“输入类型”。
输入始终是单个类型。如果要传入多个参数,可以使用结构体或元组。
// 如果只有一个参数,就使用它 let action1 = create_action(|input: &String| { let input = input.clone(); async move { todo!() } }); // 如果没有参数,则使用单元类型 `()` let action2 = create_action(|input: &()| async { todo!() }); // 如果有多个参数,则使用元组 let action3 = create_action( |input: &(usize, String)| async { todo!() } );
因为 action 函数接受一个引用,但
Future
需要具有'static
生命周期,所以你通常需要克隆该值才能将其传递给Future
。这确实很尴尬,但它解锁了一些强大的功能,如乐观 UI。我们将在后面的章节中看到更多相关内容。
所以在这种情况下,我们创建 action 所需要做的就是
let add_todo_action = create_action(|input: &String| {
let input = input.to_owned();
async move { add_todo_request(&input).await }
});
我们将使用 .dispatch()
调用它,而不是直接调用 add_todo_action
,如下所示
add_todo_action.dispatch("Some value".to_string());
你可以从事件监听器、超时或任何地方执行此操作;因为 .dispatch()
不是一个 async
函数,所以可以从同步上下文中调用它。
Action 提供对一些信号的访问,这些信号在你要调用的异步 action 和同步响应式系统之间进行同步:
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”变得很容易。
let input_ref = create_node_ref::<Input>();
view! {
<form
on:submit=move |ev| {
ev.prevent_default(); // 不要重新加载页面...
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>
// 使用我们的加载状态
<p>{move || pending().then("Loading...")}</p>
}
现在,有可能这一切看起来有点过于复杂,或者可能限制太多。我想在这里将 action 与 resource 一起包含进来,作为拼图中缺失的一块。在一个真实的 Leptos 应用程序中,你实际上最常将 action 与服务器函数 create_server_action
和 <ActionForm/>
组件一起使用,以创建真正强大的渐进增强表单。所以如果这个原语对你来说似乎毫无用处... 不要担心!也许以后会有意义。(或者现在就查看我们的 todo_app_sqlite
示例。)
实时示例
CodeSandbox 源码
use gloo_timers::future::TimeoutFuture;
use leptos::{html::Input, *};
use uuid::Uuid;
// 这里我们定义一个异步函数
// 这可以是任何东西:网络请求、数据库读取等等。
// 将其视为一个修改:你运行的某个命令式异步操作,
// 而 resource 将是你加载的一些异步数据
async fn add_todo(text: &str) -> Uuid {
_ = text;
// 模拟一秒钟的延迟
TimeoutFuture::new(1_000).await;
// 假装这是一个帖子 ID 或其他东西
Uuid::new_v4()
}
#[component]
fn App() -> impl IntoView {
// action 接受一个带有一个参数的异步函数
// 它可以是一个简单类型、一个结构体或 ()
let add_todo = create_action(|input: &String| {
// 输入是一个引用,但我们需要 Future 拥有它
// 这很重要:我们需要克隆并移动到 Future 中
// 这样它就有一个 'static 生命周期
let input = input.to_owned();
async move { add_todo(&input).await }
});
// action 提供了一堆同步的、响应式变量
// 这些变量告诉我们关于 action 状态的不同信息
let submitted = add_todo.input();
let pending = add_todo.pending();
let todo_id = add_todo.value();
let input_ref = create_node_ref::<Input>();
view! {
<form
on:submit=move |ev| {
ev.prevent_default(); // 不要重新加载页面...
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().then(|| "Loading...")}</p>
<p>
"Submitted: "
<code>{move || format!("{:#?}", submitted())}</code>
</p>
<p>
"Pending: "
<code>{move || format!("{:#?}", pending())}</code>
</p>
<p>
"Todo ID: "
<code>{move || format!("{:#?}", todo_id())}</code>
</p>
}
}
fn main() {
leptos::mount_to_body(App)
}