Projecting Children
As you build components you may occasionally find yourself wanting to “project” children through multiple layers of components.
The Problem
Consider the following:
pub fn NestedShow<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn() -> IV + Send + Sync + 'static,
IV: IntoView + 'static,
{
view! {
<Show
when=|| todo!()
fallback=|| ()
>
<Show
when=|| todo!()
fallback=fallback
>
{children()}
</Show>
</Show>
}
}
This is pretty straightforward: if the inner condition is true
, we want to show children
. If not, we want to show fallback
. And if the outer condition is false
, we just render ()
, i.e., nothing.
In other words, we want to pass the children of <NestedShow/>
through the outer <Show/>
component to become the children of the inner <Show/>
. This is what I mean by “projection.”
This won’t compile.
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
Each <Show/>
needs to be able to construct its children
multiple times. The first time you construct the outer <Show/>
’s children, it takes fallback
and children
to move them into the invocation of the inner <Show/>
, but then they're not available for future outer-<Show/>
children construction.
The Details
Feel free to skip ahead to the solution.
If you want to really understand the issue here, it may help to look at the expanded view
macro. Here’s a cleaned-up version:
Show(
ShowProps::builder()
.when(|| todo!())
.fallback(|| ())
.children({
// children and fallback are moved into a closure here
::leptos::children::ToChildren::to_children(move || {
Show(
ShowProps::builder()
.when(|| todo!())
// fallback is consumed here
.fallback(fallback)
.children({
// children is captured here
::leptos::children::ToChildren::to_children(
move || children(),
)
})
.build(),
)
})
})
.build(),
)
All components own their props; so the <Show/>
in this case can’t be called because it only has captured references to fallback
and children
.
Solution
However, both <Suspense/>
and <Show/>
take ChildrenFn
, i.e., their children
should implement the Fn
type so they can be called multiple times with only an immutable reference. This means we don’t need to own children
or fallback
; we just need to be able to pass 'static
references to them.
We can solve this problem by using the StoredValue
primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, Copy
and 'static
, which we can access or modify through certain methods.
In this case, it’s really simple:
pub fn NestedShow<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn() -> IV + Send + Sync + 'static,
IV: IntoView + 'static,
{
let fallback = StoredValue::new(fallback);
let children = StoredValue::new(children);
view! {
<Show
when=|| todo!()
fallback=|| ()
>
<Show
// check whether user is verified
// by reading from the resource
when=move || todo!()
fallback=move || fallback.read_value()()
>
{children.read_value()()}
</Show>
</Show>
}
}
At the top level, we store both fallback
and children
in the reactive scope owned by LoggedIn
. Now we can simply move those references down through the other layers into the <Show/>
component and call them there.
A Final Note
Note that this works because <Show/>
only needs an immutable reference to their children (which .read_value
can give), not ownership.
In other cases, you may need to project owned props through a function that takes ChildrenFn
and therefore needs to be called more than once. In this case, you may find the clone:
helper in theview
macro helpful.
Consider this example
#[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>
}
}
Even with name=name.clone()
, this gives the error
cannot move out of `name`, a captured variable in an `Fn` closure
It’s captured through multiple levels of children that need to run more than once, and there’s no obvious way to clone it into the children.
In this case, the clone:
syntax comes in handy. Calling clone:name
will clone name
before moving it into <Inner/>
’s children, which solves our ownership issue.
view! {
<Outer>
<Inner clone:name>
<Inmost name=name.clone()/>
</Inner>
</Outer>
}
These issues can be a little tricky to understand or debug, because of the opacity of the view
macro. But in general, they can always be solved.