投影子组件
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>
}
}
这逻辑非常直观:如果内部条件为 true,我们希望显示 children。如果不是,我们希望显示 fallback。如果外部条件为 false,我们只渲染 (),即不显示任何内容。
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.
换句话说,我们希望将 <NestedShow/> 的子组件穿过外部的 <Show/> 组件,成为内部 <Show/> 的子组件。这就是我所说的“投影”。
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`
每个 <Show/> 都需要能够多次构造其 children。当你第一次构造外部 <Show/> 的子组件时,它会获取 fallback 和 children 的所有权并将它们移动到内部 <Show/> 的调用中,但随后它们在未来的外部 <Show/> 子组件构造中就不可用了。
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.
如果你想真正理解这个问题,查看展开后的 view 宏可能会有所帮助。这是一个清理后的版本:
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 和 fallback 被移动到了闭包中
// children and fallback are moved into a closure here
::leptos::children::ToChildren::to_children(move || {
Show(
ShowProps::builder()
.when(|| todo!())
// fallback 在这里被消耗了
// fallback is consumed here
.fallback(fallback)
.children({
// children 在这里被捕获
// children is captured here
::leptos::children::ToChildren::to_children(
move || children(),
)
})
.build(),
)
})
})
.build(),
)
所有组件都拥有它们属性(props)的所有权;因此在这种情况下无法调用 <Show/>,因为它只捕获了对 fallback 和 children 的引用。
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
然而,<Suspense/> 和 <Show/> 都接收 ChildrenFn,也就是说,它们的 children 应该实现 Fn trait,以便只需不可变引用即可多次调用。这意味着我们不需要拥有 children 或 fallback 的所有权;我们只需要能够传递对它们的 'static 引用。
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.
我们可以使用 StoredValue 原语来解决这个问题。它本质上是在响应式系统中存储一个值,将所有权移交给框架,以换取一个像信号(signal)一样具有 Copy 和 'static 特性的引用,我们可以通过某些方法来访问或修改它。
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>
}
}
在顶层,我们将 fallback 和 children 存储在由 NestedShow 拥有的响应式作用域中。现在我们可以简单地将这些引用向下传递到其他层级中的 <Show/> 组件,并在那里调用它们。
At the top level, we store both fallback and children in the reactive scope owned by NestedShow. Now we can simply move those references down through the other layers into the <Show/> component and call them there.
最后一点说明
A Final Note
请注意,这之所以有效,是因为 <Show/> 只需要对其子组件的不可变引用(.read_value 可以提供),而不需要所有权。
Note that this works because <Show/> only needs an immutable reference to their children (which .read_value can give), not ownership.
在其他情况下,你可能需要将拥有的属性(props)投影到一个接收 ChildrenFn 且因此需要多次调用的函数中。在这种情况下,你会发现 view 宏中的 clone: 辅助语法非常有用。
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>
}
}
即使使用了 name=name.clone(),这也会报错:
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.
在这种情况下,clone: 语法就派上用场了。调用 clone:name 会在将 name 移动到 <Inner/> 的子组件 之前 对其进行克隆,这解决了我们的所有权问题。
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>
}
由于 view 宏的不透明性,这些问题可能有点难以理解或调试。但总的来说,它们总是可以被解决的。
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.