Порядок выполнения
В большинстве приложений вам порой нужно принять решение: Мне рендерить это как часть view или нет?
Мне рендерить <ButtonA/>
или <WidgetB/>
. Это и есть порядок выполнения (англ. control flow).
Несколько советов
Думая о том, как сделать это в Leptos, важно помнить несколько вещей:
- Rust это язык ориентированный на выражения: такие выражения порядка выполнения, как
if x() { y } else { z }
иmatch x() { ... }
возвращают свои значения. Это делает их очень полезными для декларативных пользовательских интерфейсов. - Для любого
T
реализующегоIntoView
— другими словами, для любого типа, который Leptos может рендерить —Option<T>
иResult<T, impl Error>
тоже реализуютIntoView
. И так же какFn() -> T
рендерит реактивныйT
,Fn() -> Option<T>
иFn() -> Result<T, impl Error>
тоже реактивны. - В Rust есть множество полезных хелперов, таких как Option::map,
Option::and_then,
Option::ok_or,
Result::map,
Result::ok, и
bool::then, позволяющих вам декларативно конвертировать значения между несколькими стандартными типами, все из которых
могут быть отрендерены. Потратить время на чтение документации к
Option
иResult
это один из лучших способов выйти на новый уровень в Rust. - И всегда помните: чтобы быть реактивными, значения должны быть функциями. Ниже будет видно, как я постоянно оборачиваю вещи в
move ||
замыкания. Это чтобы убедиться, что они действительно выполняются повторно когда меняется сигнал, от которого они зависят, поддерживая реактивность UI.
Ну и что?
Чтобы немного собрать всё воедино: на практике это значит, что большую часть контроля над порядком выполнения можно реализовать при помощи нативного Rust кода, без особых компонентов, управляющих порядком выполнения и не обладая специальными знаниями.
К примеру, давайте начнём с простого сигнала и производного сигнала:
let (value, set_value) = create_signal(0);
let is_odd = move || value() & 1 == 1;
Если вы не понимаете что происходит с
is_odd
, то не волнуйтесь. Это лишь простой способ проверить число на нечётность, используя побитовое «И» с 1.
Мы можем использовать эти сигналы и обычный Rust чтобы реализовать большую часть управления порядком выполнения.
Оператор if
Предположим, я хочу выводить одну надпись если число нечётно и другую если оно чётно. Что скажете вот об этом?
view! {
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
}
Выражение if
возвращает своё значение, а &str
реализует IntoView
, поэтому
Fn() -> &str
реализует IntoView
, так что это... просто работает!
Option<T>
Предположим мы хотим выводить надпись если число нечётно и ничего не выводить, если чётно.
let message = move || {
if is_odd() {
Some("Ding ding ding!")
} else {
None
}
};
view! {
<p>{message}</p>
}
Это отлично работает. Мы можем, как вариант, немного сократить код, используя bool::then()
.
let message = move || is_odd().then(|| "Ding ding ding!");
view! {
<p>{message}</p>
}
При желании можно даже обойтись без переменной. Хотя, лично я порой предпочитаю улучшенную поддержку cargo fmt
и rust-analyzer
,
которую получаешь, вынося такое за пределы view
.
Конструкция match
Мы всё ещё пишем код на обычном Rust, ведь так? Это означает, у вас в распоряжении вся мощь сопоставления с образцом, доступного в Rust.
let message = move || {
match value() {
0 => "Zero",
1 => "One",
n if is_odd() => "Odd",
_ => "Even"
}
};
view! {
<p>{message}</p>
}
А почему бы и нет? Живём только раз, не так ли?
Предотвращение чрезмерного рендеринга
А вот и не только раз.
Всё что мы только что сделали это в целом нормально. Но один нюанс, который вам стоит запомнить и обходиться с ним аккуратно. Каждая из функций управляющих порядком выполнения, из тех что мы создали к этому моменту, это в сущности производный сигнал: они будут перезапускаться при каждом изменении значения. В примерах выше, где значение меняется с четного на нечётное при каждом изменении, это нормально.
Но рассмотрим следующий пример:
let (value, set_value) = create_signal(0);
let message = move || if value() > 5 {
"Big"
} else {
"Small"
};
view! {
<p>{message}</p>
}
Это конечно работает. Но если добавить вывод в лог, то можно удивиться
let message = move || if value() > 5 {
logging::log!("{}: rendering Big", value());
"Big"
} else {
logging::log!("{}: rendering Small", value());
"Small"
};
Когда пользователь нажимает на кнопку будет что-то вроде этого:
1: rendering Small
2: rendering Small
3: rendering Small
4: rendering Small
5: rendering Small
6: rendering Big
7: rendering Big
8: rendering Big
... и так до бесконечности
Каждый раз когда value
меняется, if
выполняется снова. Оно и понятно, учитывая как устроена реактивность. Но у этого есть недостаток.
Для простого текстового узла повторное выполнение if
и повторный рендеринг это пустяки. Но представьте если бы было так:
let message = move || if value() > 5 {
<Big/>
} else {
<Small/>
};
<Small/>
перезапускается пять раз, затем <Big/>
бесконечно. Если они подгружают ресурсы, создают сигналы или даже просто создают узлы DOM,
это лишняя работа.
<Show/>
Компонент <Show/>
это ответ этим проблемам. Вы передаёте условную функцию when
и fallback
для показа в случае если when
вернула false
,
и дочерние элементы, которые будут отображаться если when
равно true
.
let (value, set_value) = create_signal(0);
view! {
<Show
when=move || { value() > 5 }
fallback=|| view! { <Small/> }
>
<Big/>
</Show>
}
<Show/>
мемоизирует условие when
, так что он рендерит <Small/>
лишь раз,
продолжая показывать тот же компонент пока value
не станет больше пяти;
затем он отрендерит <Big/>
один раз, продолжая его показывать вечно или пока value
не станет меньше пяти, тогда он снова отрендерит <Small/>
.
Это полезный инструмент чтобы избежать повторного рендеринга при использовании динамических if
выражений.
Как обычно, есть и накладные расходы: для очень маленького узла (как обновление одного текстового узла, обновления класса или атрибута)
конструкция move || if ...
будет менее затратна. Но если рендеринг какой-либо ветки хоть сколько-нибудь затратен,
используйте <Show/>
.
Заметка: конвертация типов
Это последняя вещь, о которой важно сказать в этом разделе.
Макрос view
не возвращает самый обобщенный оборачивающий тип View
.
Вместо этого, он возвращает такие типы как Fragment
или HtmlElement<Input>
. Это может немного раздражать, если
возвращать разные элементы HTML из разных ветвей условия.
view! {
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { <pre>"One"</pre> }
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { <p>"Two"</p> }
}
// returns HtmlElement<Textarea>
_ => view! { <textarea>{value()}</textarea> }
}}
</main>
}
Эта сильная типизация в самом деле мощная штука, поскольку HtmlElement
кроме всего прочего является умным указателем:
каждый тип HtmlElement<T>
реализует Deref
для подходящего внутреннего типа web_sys
. Другими словами, view
в браузере
возвращает настоящие DOM элементы и у них можно вызывать нативные методы DOM.
Но это может немного раздражать в условной логике как здесь, поскольку в Rust вы не можете возвращать разные типы из разных ветвей условия. Есть два выхода из положения:
- Если у вас несколько
HtmlElement
типов, конвертируйте их вHtmlElement<AnyElement>
при помощи.into_any()
- Если у вас набор view-типов, не все из которых
HtmlElement
, конвертируйте их воView
через.into_view()
.
Вот тот же пример, с добавленной конвертацией:
view! {
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { <p>"Two"</p> }.into_any()
}
// returns HtmlElement<Textarea>
_ => view! { <textarea>{value()}</textarea> }.into_any()
}}
</main>
}
[Нажмите, чтобы открыть CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-0-5-4yn7qz?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<noscript>
Please enable JavaScript to view examples.
</noscript>
<template>
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-0-5-4yn7qz?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
</template>
Код примера CodeSandbox
use leptos::*;
#[component]
fn App() -> impl IntoView {
let (value, set_value) = create_signal(0);
let is_odd = move || value() & 1 == 1;
let odd_text = move || if is_odd() { Some("How odd!") } else { None };
view! {
<h1>"Control Flow"</h1>
// Simple UI to update and show a value
<button on:click=move |_| set_value.update(|n| *n += 1)>
"+1"
</button>
<p>"Value is: " {value}</p>
<hr/>
<h2><code>"Option<T>"</code></h2>
// For any `T` that implements `IntoView`,
// so does `Option<T>`
<p>{odd_text}</p>
// This means you can use `Option` methods on it
<p>{move || odd_text().map(|text| text.len())}</p>
<h2>"Conditional Logic"</h2>
// You can do dynamic conditional if-then-else
// logic in several ways
//
// a. An "if" expression in a function
// This will simply re-render every time the value
// changes, which makes it good for lightweight UI
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
// b. Toggling some kind of class
// This is smart for an element that's going to
// toggled often, because it doesn't destroy
// it in between states
// (you can find the `hidden` class in `index.html`)
<p class:hidden=is_odd>"Appears if even."</p>
// c. The <Show/> component
// This only renders the fallback and the child
// once, lazily, and toggles between them when
// needed. This makes it more efficient in many cases
// than a {move || if ...} block
<Show when=is_odd
fallback=|| view! { <p>"Even steven"</p> }
>
<p>"Oddment"</p>
</Show>
// d. Because `bool::then()` converts a `bool` to
// `Option`, you can use it to create a show/hide toggled
{move || is_odd().then(|| view! { <p>"Oddity!"</p> })}
<h2>"Converting between Types"</h2>
// e. Note: if branches return different types,
// you can convert between them with
// `.into_any()` (for different HTML element types)
// or `.into_view()` (for all view types)
{move || match is_odd() {
true if value() == 1 => {
// <pre> returns HtmlElement<Pre>
view! { <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// <p> returns HtmlElement<P>
// so we convert into a more generic type
view! { <p>"Two"</p> }.into_any()
}
_ => view! { <textarea>{value()}</textarea> }.into_any()
}}
}
}
fn main() {
leptos::mount_to_body(App)
}