Проброс дочерних элементов
При написании компонентов можно время от времени ловить себя на желании "пробросить" через несколько уровней компонентов.
Задача
Рассмотрим следующий пример:
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn() -> IV + 'static,
IV: IntoView,
{
view! {
<Suspense
fallback=|| ()
>
<Show
// check whether user is verified
// by reading from the resource
when=move || todo!()
fallback=fallback
>
{children()}
</Show>
</Suspense>
}
}
Он достаточно прост: когда пользователь авторизован, выводится children. Когда нет — выводится fallback.
А пока информация не пришла, выводится (), т.е. ничего.
Другими словами, дочерние элементы <LoggedIn/> хочется передать через компонент <Suspense/>,
чтобы они стали дочерними элементами <Show/>. Это то, что имеется в виду под "пробросом".
Это не скомпилируется.
error[E0507]: cannot move out of `fallback`, a captured variable in an `Fn` closure
error[E0507]: cannot move out of `children`, a captured variable in an `Fn` closure
Проблема в том, что компонентам <Suspense/> и <Show/> нужна возможность строить свои children несколько раз.
При первом построении дочерних элементов <Suspense> владение fallback и children требуется,
чтобы переместить эти значения внутрь вызова <Show/>, но тогда они недоступны для последующих построений дочерних элементов <Suspense/>.
Подробности
Можете смело перейти сразу к решению.
Если хотите по-настоящему понять проблему, стоит посмотреть на расширенный макрос view. Вот подчищенный вариант:
Suspense(
::leptos::component_props_builder(&Suspense)
.fallback(|| ())
.children({
// fallback and children are moved into this closure
Box::new(move || {
{
// fallback and children captured here
leptos::Fragment::lazy(|| {
vec![
(Show(
::leptos::component_props_builder(&Show)
.when(|| true)
// but fallback is moved into Show here
.fallback(fallback)
// and children is moved into Show here
.children(children)
.build(),
)
.into_view()),
]
})
}
})
})
.build(),
)
Все компоненты владеют своими свойствами; так что <Show/> в данном случае не может быть вызван поскольку он лишь
захватил ссылки на fallback и children.
Решение
Однако, и <Suspense/> и <Show/> принимают ChildrenFn в качестве аргумента, т.е. их children должна реализовывать
тип Fn, чтобы они могли вызваться несколько раз с лишь иммутабельной ссылкой. Это означает, что владеть
children или fallback не нужно; нужно просто передать 'static' ссылки на них.
Эту проблему можно решить через примитив store_value.
Он по сути сохраняет значение в реактивной системе, передавая его во владение фреймворку, в обмен на ссылку,
которая, подобно сигналу, реализует Copy, имеет время жизни 'static, и которую с помощью определённых методов можно изменить или получить к ней доступ.
В данном примере это очень просто:
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn() -> IV + 'static,
IV: IntoView,
{
let fallback = store_value(fallback);
let children = store_value(children);
view! {
<Suspense
fallback=|| ()
>
<Show
when=|| todo!()
fallback=move || fallback.with_value(|fallback| fallback())
>
{children.with_value(|children| children())}
</Show>
</Suspense>
}
}
На верхнем уровне fallback и children сохраняется в реактивной области видимости, которой владеет LoggedIn.
Теперь можно просто переместить эти ссылки через другие уровни в компонент<Show/> и вызывать их там.
В заключение
Учтите, что это работает потому, что компонентам <Show/> и <Suspense/> нужна иммутабельная ссылка на их дочерние элементы
(которую им может дать .with_value ), а не владение ими.
В иных случаях, может понадобиться пробрасывать свойства с владением через функцию, которая принимает ChildrenFn
и которая таким образом должна вызываться более одного раза.
В этом случае может быть полезна вспомогательный синтаксис clone: в макросе view.
Рассмотрим пример
#[component]
pub fn App() -> impl IntoView {
let name = "Alice".to_string();
view! {
<Outer>
<Inner>
<Inmost name=name.clone()/>
</Inner>
</Outer>
}
}
#[component]
pub fn Outer(children: ChildrenFn) -> impl IntoView {
children()
}
#[component]
pub fn Inner(children: ChildrenFn) -> impl IntoView {
children()
}
#[component]
pub fn Inmost(name: String) -> impl IntoView {
view! {
<p>{name}</p>
}
}
Даже с name=name.clone(), возникает ошибка
cannot move out of `name`, a captured variable in an `Fn` closure
Переменная захватывается на нескольких уровнях элементов-потомков, которые должны выполняться более одного раза и нет очевидного способа клонировать её в них.
Здесь пригождается синтаксис clone:. Вызов clone:name клонирует name перед перемещением в дочерние элементы <Inner/>,
что решает проблему с владением.
view! {
<Outer>
<Inner clone:name>
<Inmost name=name.clone()/>
</Inner>
</Outer>
}
Эти проблемы могут быть немного сложны для понимания и отладки, из-за непрозрачности макроса view.
Но в целом, их всегда можно решить.