Проброс дочерних элементов

При написании компонентов можно время от времени ловить себя на желании "пробросить" через несколько уровней компонентов.

Задача

Рассмотрим следующий пример:

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. Но в целом, их всегда можно решить.