Проброс дочерних элементов
При написании компонентов можно время от времени ловить себя на желании "пробросить" через несколько уровней компонентов.
Задача
Рассмотрим следующий пример:
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
.
Но в целом, их всегда можно решить.