Skip to content

Commit

Permalink
Testing, Async (#11)
Browse files Browse the repository at this point in the history
* Testing

* Async (incomplete)

* Async (incomplete)

* async

* interlude_projecting_children.md
  • Loading branch information
kakserpom authored Jul 4, 2024
1 parent 26a0b8e commit a734def
Show file tree
Hide file tree
Showing 20 changed files with 1,069 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src-russian/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
- [Ожидания (Suspense)](./async/11_suspense.md)
- [Переходы (Transition)](./async/12_transition.md)
- [Действия (Action)](./async/13_actions.md)
- [Примечание: Пробрасывание дочерних элементов](./interlude_projecting_children.md)
- [Примечание: Проброс дочерних элементов](./interlude_projecting_children.md)
- [Управление глобальным состоянием](./15_global_state.md)
- [Маршрутизатор URL](./router/README.md)
- [Определение `<Routes/>`](./router/16_routes.md)
Expand Down
151 changes: 151 additions & 0 deletions src-russian/async/10_resources.md
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>
171 changes: 171 additions & 0 deletions src-russian/async/11_suspense.md
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>
Loading

0 comments on commit a734def

Please sign in to comment.