Мутация данных через действия (Actions)
Мы обсудили как подгружать асинхронные данные с помощью ресурсов. Ресурсы немедленно загружают данные, они тесно связаны с
компонентами <Suspense/>
и <Transition/>
, которые показывают пользователю загружаются ли данные.
Но что если хочется просто вызвать произвольную async
функцию и следить за тем, что происходит?
Ну, всегда можно использовать spawn_local
. Это позволяет породить async
задачу в синхронном окружении,
передав Future
в браузер (или, на сервере, в Tokio или в другую используемую асинхронную среду выполнения).
Но как узнать выполняется ли она всё ещё или нет? Ну, можно сделать сигнал, показывающий идёт ли загрузка, и ещё один
для показа результата...
Всё это правда. Или можно просто последний async
примитив: create_action
.
Действия и ресурсы с виду похожи, но они представляют фундаментально разные вещи. Если нужно загрузить данные через
вызов async
функции, единожды или когда какое-то другое значение меняется, то наверно стоит использовать create_resource
.
Если же нужно время от времени запускать async
функцию в ответ на, например, нажатие на кнопку, наверно стоит использовать
create_action
.
Предположим есть async
функцию, которую нужно выполнить.
async fn add_todo_request(new_title: &str) -> Uuid {
/* do some stuff on the server to add a new todo */
}
create_action
принимает в качестве аргумента async
функцию, которая в качестве аргумента принимает ссылку на
то, что можно назвать “вводный тип”.
Вводный тип всегда один. Если нужно передать несколько аргументов, это можно сделать через структуру или кортеж.
// if there's a single argument, just use that let action1 = create_action(|input: &String| { let input = input.clone(); async move { todo!() } }); // if there are no arguments, use the unit type `()` let action2 = create_action(|input: &()| async { todo!() }); // if there are multiple arguments, use a tuple let action3 = create_action( |input: &(usize, String)| async { todo!() } );
Поскольку функция действия принимает ссылку, а футуре нужно время жизни
'static
, обычно требуется клонировать значение, чтобы передать его в футуру. Это, признаться, неуклюже, но это открывает такие мощные возможности, как оптимистичный UI. Подробнее об этом в будущих главах.
Так что в данном случае всё что нужно сделать это создать действие:
let add_todo_action = create_action(|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
функция,
она может быть вызвана из синхронного контекста.
Действия дают доступ к нескольким сигналам, синхронизирующим асинхронное действия и синхронную реактивную систему:
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(); // 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().then("Loading...")}</p>
}
Что ж, всё это может показаться чуточку переусложненным или, быть может, слишком ограничивающим.
Я захотел включить сюда действия, вместе с сигналами, как недостающий пазл. В реальном приложении на Leptos,
вы действительно чаще всего будете использовать действия вместе с серверными функциями, компоненты create_server_action
,
и <ActionForm/>
для создания действительно мощных форм с прогрессивным улучшением.
Так что, если этот примитив кажется бесполезным, не переживайте! Возможно понимание придёт позже. (Или ознакомьтесь с нашим todo_app_sqlite
примером прямо сейчас.)
[Нажмите, чтобы открыть CodeSandbox.](https://codesandbox.io/p/sandbox/13-actions-0-5-8xk35v?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<noscript>
Пожалуйста, включите Javascript для просмотра примеров.
</noscript>
<template>
<iframe src="https://codesandbox.io/p/sandbox/13-actions-0-5-8xk35v?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
</template>
Код примера CodeSandbox
use gloo_timers::future::TimeoutFuture;
use leptos::{html::Input, *};
use uuid::Uuid;
// 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
TimeoutFuture::new(1_000).await;
// pretend this is a post ID or something
Uuid::new_v4()
}
#[component]
fn App() -> impl IntoView {
// an action takes an async function with single argument
// it can be a simple type, a struct, or ()
let add_todo = create_action(|input: &String| {
// 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 }
});
// 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 = create_node_ref::<Input>();
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().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)
}