Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

投影子元素

在构建组件时,您可能偶尔会发现自己想要通过多层组件"投影"子元素。

问题

考虑以下情况:

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,我们只渲染 (),即什么都不渲染。

换句话说,我们想要将 <NestedShow/> 的子元素_通过_外部 <Show/> 组件传递,成为内部 <Show/> 的子元素。这就是我所说的"投影"。

这不会编译。

error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`

每个 <Show/> 需要能够多次构造其 children。第一次构造外部 <Show/> 的子元素时,它获取 fallbackchildren 将它们移动到内部 <Show/> 的调用中,但然后它们就不可用于未来的外部 <Show/> 子元素构造了。

详细信息

随时跳到解决方案。

如果您想真正理解这里的问题,查看展开的 view 宏可能会有帮助。这是一个清理过的版本:

Show(
    ShowProps::builder()
        .when(|| todo!())
        .fallback(|| ())
        .children({
            // children 和 fallback 在这里被移动到闭包中
            ::leptos::children::ToChildren::to_children(move || {
                Show(
                    ShowProps::builder()
                        .when(|| todo!())
                        // fallback 在这里被消费
                        .fallback(fallback)
                        .children({
                            // children 在这里被捕获
                            ::leptos::children::ToChildren::to_children(
                                move || children(),
                            )
                        })
                        .build(),
                )
            })
        })
        .build(),
)

所有组件都拥有它们的 props;所以在这种情况下 <Show/> 不能被调用,因为它只捕获了对 fallbackchildren 的引用。

解决方案

但是,<Suspense/><Show/> 都接受 ChildrenFn,即它们的 children 应该实现 Fn 类型,这样它们可以只用不可变引用被多次调用。这意味着我们不需要拥有 childrenfallback;我们只需要能够传递对它们的 'static 引用。

我们可以通过使用 StoredValue 原语来解决这个问题。这本质上将值存储在响应式系统中,将所有权交给框架,换取一个像 signals 一样是 Copy'static 的引用,我们可以通过某些方法访问或修改它。

在这种情况下,这真的很简单:

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
                // 通过从 resource 读取来检查用户是否已验证
                when=move || todo!()
                fallback=move || fallback.read_value()()
            >
                {children.read_value()()}
            </Show>
        </Show>
    }
}

在顶层,我们将 fallbackchildren 都存储在 NestedShow 拥有的响应式作用域中。现在我们可以简单地将这些引用向下移动通过其他层到 <Show/> 组件中并在那里调用它们。

最后说明

注意这之所以有效,是因为 <Show/> 只需要对其子元素的不可变引用(.read_value 可以提供),而不是所有权。

在其他情况下,您可能需要通过一个接受 ChildrenFn 的函数投影拥有的 props,因此需要被多次调用。在这种情况下,您可能会发现 view 宏中的 clone: 助手很有用。

考虑这个示例

#[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 宏的不透明性,这些问题可能有点难以理解或调试。但一般来说,它们总是可以解决的。