Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

使用 Actions 修改数据

我们已经讨论了如何使用 resources 加载 async 数据。Resources 立即加载数据并与 <Suspense/><Transition/> 组件密切配合,以显示您的应用程序中数据是否正在加载。但是如果您只想调用一些任意的 async 函数并跟踪它在做什么呢?

好吧,您总是可以使用 spawn_local。这允许您通过将 Future 交给浏览器(或在服务器上,Tokio 或您正在使用的任何其他运行时)在同步环境中生成 async 任务。但是您如何知道它是否仍在等待?好吧,您可以设置一个 signal 来显示它是否正在加载,另一个来显示结果...

所有这些都是正确的。或者您可以使用最终的 async 原语:Action

Actions 和 resources 看起来相似,但它们代表根本不同的东西。如果您试图通过运行 async 函数来加载数据,无论是一次还是当其他值更改时,您可能想要使用 resource。如果您试图偶尔运行 async 函数以响应用户点击按钮之类的事情,您可能想要使用 Action

假设我们有一些想要运行的 async 函数。

async fn add_todo_request(new_title: &str) -> Uuid {
    /* 在服务器上做一些事情来添加新的 todo */
}

Action::new() 接受一个 async 函数,该函数接受对单个参数的引用,您可以将其视为"输入类型"。

输入始终是单一类型。如果您想传入多个参数,可以使用结构体或元组来完成。

// 如果有单个参数,只需使用它
let action1 = Action::new(|input: &String| {
   let input = input.clone();
   async move { todo!() }
});

// 如果没有参数,使用单元类型 `()`
let action2 = Action::new(|input: &()| async { todo!() });

// 如果有多个参数,使用元组
let action3 = Action::new(
  |input: &(usize, String)| async { todo!() }
);

因为 action 函数接受引用但 Future 需要有 'static 生命周期,您通常需要克隆值以将其传递到 Future 中。这确实有点笨拙,但它解锁了一些强大的功能,如乐观 UI。我们将在未来的章节中看到更多相关内容。

所以在这种情况下,我们创建 action 所需要做的就是

let add_todo_action = Action::new(|input: &String| {
    let input = input.to_owned();
    async move { add_todo_request(&input).await }
});

我们不会直接调用 add_todo_action,而是使用 .dispatch() 调用它,如

add_todo_action.dispatch("Some value".to_string());

您可以从事件监听器、超时或任何地方执行此操作;因为 .dispatch() 不是 async 函数,它可以从同步上下文中调用。

Actions 提供对几个 signals 的访问,这些 signals 在您调用的异步 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 = NodeRef::<Input>::new();

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.get().then_some("Loading...")}</p>
}

现在,这一切可能看起来有点过于复杂,或者可能过于受限。我想在这里包含 actions,与 resources 一起,作为拼图的缺失部分。在真正的 Leptos 应用程序中,您实际上最常将 actions 与 server functions、ServerAction<ActionForm/> 组件一起使用,以创建真正强大的渐进增强表单。所以如果这个原语对您来说似乎没用...不要担心!也许稍后会有意义。(或者现在查看我们的 todo_app_sqlite 示例。)

Live example

点击打开 CodeSandbox。

CodeSandbox 源码
use gloo_timers::future::TimeoutFuture;
use leptos::{html::Input, prelude::*};
use uuid::Uuid;

// 在这里我们定义一个异步函数
// 这可以是任何东西:网络请求、数据库读取等
// 将其视为变更:您运行的一些命令式异步操作,
// 而 resource 将是您加载的一些异步数据
async fn add_todo(text: &str) -> Uuid {
    _ = text;
    // 模拟一秒延迟
    // SendWrapper 允许我们使用这个 !Send 浏览器 API;不用担心
    send_wrapper::SendWrapper::new(TimeoutFuture::new(1_000)).await;
    // 假装这是一个帖子 ID 或其他什么
    Uuid::new_v4()
}

#[component]
pub fn App() -> impl IntoView {
    // action 接受一个带有单个参数的异步函数
    // 它可以是简单类型、结构体或 ()
    let add_todo = Action::new(|input: &String| {
        // 输入是引用,但我们需要 Future 拥有它
        // 这很重要:我们需要克隆并移动到 Future 中
        // 这样它就有了 'static 生命周期
        let input = input.to_owned();
        async move { add_todo(&input).await }
    });

    // actions 提供一堆同步的响应式变量
    // 告诉我们 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(); // 不要重新加载页面...
                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)
}