-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Testing * Async (incomplete) * Async (incomplete) * async * interlude_projecting_children.md
- Loading branch information
Showing
20 changed files
with
1,069 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
# Подгрузка данных с помощью ресурсов | ||
|
||
[Ресурс (Resource)](https://docs.rs/leptos/latest/leptos/struct.Resource.html) это реактивная структура данных, отражающая текущее состояние асинхронной задачи, и | ||
позволяющая интегрировать асинхронные футуры (`Future`) в синхронную реактивную систему. Вместо ожидания данных через `.await`, | ||
мы превращаем футуру в сигнал, возвращающий `Some(T)` если она уже разрешилась и `None` если она ещё в ожидании. | ||
|
||
Это делается с помощью функции [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html). Она принимает два аргумента: | ||
|
||
1. сигнал-источник, порождающий новую футуру при любом изменении | ||
2. функция, принимающая как аргумент данные из сигнала и возвращающая футуру | ||
|
||
Вот пример: | ||
|
||
```rust | ||
// our source signal: some synchronous, local state | ||
let (count, set_count) = create_signal(0); | ||
|
||
// our resource | ||
let async_data = create_resource( | ||
count, | ||
// every time `count` changes, this will run | ||
|value| async move { | ||
logging::log!("loading data from API"); | ||
load_data(value).await | ||
}, | ||
); | ||
``` | ||
|
||
Для создания ресурса, который выполняется единожды, можно передать нереактивный, пустой сигнал-источник: | ||
|
||
```rust | ||
let once = create_resource(|| (), |_| async move { load_data().await }); | ||
``` | ||
|
||
Для доступа к значению можно использовать `.get()` или `.with(|data| /* */)`. Они работают так же как `.get()` и `.with()` | ||
у сигнала: `get` возвращает клонированное значение, а `with` применяет замыкание. Однако для всякого `Resource<_, T>`, | ||
они возвращают `Option<T>`, а не `T`, поскольку всегда возможно, что ресурс всё ещё грузится. | ||
|
||
Так можно отобразить текущее состояние ресурса во `view`: | ||
|
||
```rust | ||
let once = create_resource(|| (), |_| async move { load_data().await }); | ||
view! { | ||
<h1>"My Data"</h1> | ||
{move || match once.get() { | ||
None => view! { <p>"Loading..."</p> }.into_view(), | ||
Some(data) => view! { <ShowData data/> }.into_view() | ||
}} | ||
} | ||
``` | ||
|
||
Ресурсы также предоставляют метод `refetch()`, позволяющий вручную перезапросить данные (например, в ответ на нажатие кнопки) | ||
и метод `loading()`, возвращающий `ReadSignal<bool>` — индикатор того загружается ли в данный момент сигнал. | ||
|
||
```admonish sandbox title="Live example" collapsible=true | ||
[Нажмите чтобы открыть CodeSandbox.](https://codesandbox.io/p/sandbox/10-resources-0-5-x6h5j6?file=%2Fsrc%2Fmain.rs%3A2%2C3) | ||
<noscript> | ||
Пожалуйста, включите Javascript для просмотра примеров. | ||
</noscript> | ||
<template> | ||
<iframe src="https://codesandbox.io/p/sandbox/10-resources-0-5-9jq86q?file=%2Fsrc%2Fmain.rs%3A2%2C3" width="100%" height="1000px" style="max-height: 100vh"></iframe> | ||
</template> | ||
``` | ||
|
||
<details> | ||
<summary>Код примера CodeSandbox</summary> | ||
|
||
```rust | ||
use gloo_timers::future::TimeoutFuture; | ||
use leptos::*; | ||
|
||
// Here we define an async function | ||
// This could be anything: a network request, database read, etc. | ||
// Here, we just multiply a number by 10 | ||
async fn load_data(value: i32) -> i32 { | ||
// fake a one-second delay | ||
TimeoutFuture::new(1_000).await; | ||
value * 10 | ||
} | ||
|
||
#[component] | ||
fn App() -> impl IntoView { | ||
// this count is our synchronous, local state | ||
let (count, set_count) = create_signal(0); | ||
|
||
// create_resource takes two arguments after its scope | ||
let async_data = create_resource( | ||
// the first is the "source signal" | ||
count, | ||
// the second is the loader | ||
// it takes the source signal's value as its argument | ||
// and does some async work | ||
|value| async move { load_data(value).await }, | ||
); | ||
// whenever the source signal changes, the loader reloads | ||
|
||
// you can also create resources that only load once | ||
// just return the unit type () from the source signal | ||
// that doesn't depend on anything: we just load it once | ||
let stable = create_resource(|| (), |_| async move { load_data(1).await }); | ||
|
||
// we can access the resource values with .get() | ||
// this will reactively return None before the Future has resolved | ||
// and update to Some(T) when it has resolved | ||
let async_result = move || { | ||
async_data | ||
.get() | ||
.map(|value| format!("Server returned {value:?}")) | ||
// This loading state will only show before the first load | ||
.unwrap_or_else(|| "Loading...".into()) | ||
}; | ||
|
||
// the resource's loading() method gives us a | ||
// signal to indicate whether it's currently loading | ||
let loading = async_data.loading(); | ||
let is_loading = move || if loading() { "Loading..." } else { "Idle." }; | ||
|
||
view! { | ||
<button | ||
on:click=move |_| { | ||
set_count.update(|n| *n += 1); | ||
} | ||
> | ||
"Click me" | ||
</button> | ||
<p> | ||
<code>"stable"</code>": " {move || stable.get()} | ||
</p> | ||
<p> | ||
<code>"count"</code>": " {count} | ||
</p> | ||
<p> | ||
<code>"async_value"</code>": " | ||
{async_result} | ||
<br/> | ||
{is_loading} | ||
</p> | ||
} | ||
} | ||
|
||
fn main() { | ||
leptos::mount_to_body(App) | ||
} | ||
``` | ||
|
||
</details> | ||
</preview> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
# `<Suspense/>` | ||
|
||
В предыдущей главе было рассмотрено создание простого экрана загрузки, чтобы показывать некий `fallback` пока ресурс грузится. | ||
|
||
```rust | ||
let (count, set_count) = create_signal(0); | ||
let once = create_resource(count, |count| async move { load_a(count).await }); | ||
|
||
view! { | ||
<h1>"My Data"</h1> | ||
{move || match once.get() { | ||
None => view! { <p>"Loading..."</p> }.into_view(), | ||
Some(data) => view! { <ShowData data/> }.into_view() | ||
}} | ||
} | ||
``` | ||
|
||
Но что если есть два ресурса и хочется ожидать оба? | ||
|
||
```rust | ||
let (count, set_count) = create_signal(0); | ||
let (count2, set_count2) = create_signal(0); | ||
let a = create_resource(count, |count| async move { load_a(count).await }); | ||
let b = create_resource(count2, |count| async move { load_b(count).await }); | ||
|
||
view! { | ||
<h1>"My Data"</h1> | ||
{move || match (a.get(), b.get()) { | ||
(Some(a), Some(b)) => view! { | ||
<ShowA a/> | ||
<ShowA b/> | ||
}.into_view(), | ||
_ => view! { <p>"Loading..."</p> }.into_view() | ||
}} | ||
} | ||
``` | ||
|
||
Выглядит не _очень_ плохо, но всё же несколько раздражает. Что если использовать инверсию управления (IoC)? | ||
|
||
Компонент [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) позволяет сделать именно это. Вы передаёте ему свойство `fallback` и дочерние элементы, | ||
один или более из которых обычно включает в себя чтение из ресурса. Чтение из ресурса “внутри” a `<Suspense/>` | ||
(т.е. в одном из элементов-потомков) регистрирует ресурс в `<Suspense/>`. Если загрузка хотя бы одного из ресурсов всё ещё идет, | ||
отображается `fallback`. Когда они все загружены, отображаются дочерние элементы. | ||
|
||
```rust | ||
let (count, set_count) = create_signal(0); | ||
let (count2, set_count2) = create_signal(0); | ||
let a = create_resource(count, |count| async move { load_a(count).await }); | ||
let b = create_resource(count2, |count| async move { load_b(count).await }); | ||
|
||
view! { | ||
<h1>"My Data"</h1> | ||
<Suspense | ||
fallback=move || view! { <p>"Loading..."</p> } | ||
> | ||
<h2>"My Data"</h2> | ||
<h3>"A"</h3> | ||
{move || { | ||
a.get() | ||
.map(|a| view! { <ShowA a/> }) | ||
}} | ||
<h3>"B"</h3> | ||
{move || { | ||
b.get() | ||
.map(|b| view! { <ShowB b/> }) | ||
}} | ||
</Suspense> | ||
} | ||
``` | ||
|
||
Каждый раз когда любой из ресурсов перезагружается, `fallback` с "Loading..." отображается снова. | ||
|
||
Инверсия управления упрощает добавление и удаление отдельных ресурсов, избавляя от необходимости самостоятельно делать сопоставление с шаблоном. | ||
Это также открывает значительные оптимизации производительности во время серверного рендеринга (SSR), о которых мы поговорим в одной из следующих глав. | ||
|
||
## `<Await/>` | ||
|
||
Если хочется просто дождаться какого-то футуры перед рендерингом, компонент `<Await/>` может быть полезен в уменьшении шаблонного кода. | ||
`<Await/>` по сути сочетает в себе ресурс с источником `|| ()` и `<Suspense/>` без `fallback`. | ||
|
||
Другими словами: | ||
|
||
1. Он poll'ит `Future` лишь раз и не реагирует ни на какие реактивные изменения. | ||
2. Он ничего не рендерит пока `Future` не разрешится. | ||
3. После разрешения футуры, он кладёт её данные в выбранную переменную и рендерит дочерние элементы с этой переменной в области видимости. | ||
|
||
```rust | ||
async fn fetch_monkeys(monkey: i32) -> i32 { | ||
// maybe this didn't need to be async | ||
monkey * 2 | ||
} | ||
view! { | ||
<Await | ||
// `future` provides the `Future` to be resolved | ||
future=|| fetch_monkeys(3) | ||
// the data is bound to whatever variable name you provide | ||
let:data | ||
> | ||
// you receive the data by reference and can use it in your view here | ||
<p>{*data} " little monkeys, jumping on the bed."</p> | ||
</Await> | ||
} | ||
``` | ||
|
||
```admonish sandbox title="Live example" collapsible=true | ||
[Нажмите, чтобы открыть CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-0-5-qzpgqs?file=%2Fsrc%2Fmain.rs%3A1%2C1) | ||
<noscript> | ||
Please enable JavaScript to view examples. | ||
</noscript> | ||
<template> | ||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-0-5-qzpgqs?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> | ||
</template> | ||
``` | ||
|
||
<details> | ||
<summary>Код примера CodeSandbox</summary> | ||
|
||
```rust | ||
use gloo_timers::future::TimeoutFuture; | ||
use leptos::*; | ||
|
||
async fn important_api_call(name: String) -> String { | ||
TimeoutFuture::new(1_000).await; | ||
name.to_ascii_uppercase() | ||
} | ||
|
||
#[component] | ||
fn App() -> impl IntoView { | ||
let (name, set_name) = create_signal("Bill".to_string()); | ||
|
||
// this will reload every time `name` changes | ||
let async_data = create_resource( | ||
|
||
name, | ||
|name| async move { important_api_call(name).await }, | ||
); | ||
|
||
view! { | ||
<input | ||
on:input=move |ev| { | ||
set_name(event_target_value(&ev)); | ||
} | ||
prop:value=name | ||
/> | ||
<p><code>"name:"</code> {name}</p> | ||
<Suspense | ||
// the fallback will show whenever a resource | ||
// read "under" the suspense is loading | ||
fallback=move || view! { <p>"Loading..."</p> } | ||
> | ||
// the children will be rendered once initially, | ||
// and then whenever any resources has been resolved | ||
<p> | ||
"Your shouting name is " | ||
{move || async_data.get()} | ||
</p> | ||
</Suspense> | ||
} | ||
} | ||
|
||
fn main() { | ||
leptos::mount_to_body(App) | ||
} | ||
``` | ||
|
||
</details> | ||
</preview> |
Oops, something went wrong.