leptos icon indicating copy to clipboard operation
leptos copied to clipboard

DynChild is populated as a single Child in the components' children property

Open gzp79 opened this issue 2 years ago • 3 comments

Describe the bug Generating children dynamically is problematic. The list of children are sent as a single View instead of a list of views. Please see the sample code for details.

leptos = { version = "0.6", features = ["csr"] }

To Reproduce

use leptos::{component, view, Children, CollectView, IntoView};


#[component]
pub fn WrapsChildren(children: Children) -> impl IntoView {
    let nodes = children().nodes;
    log::info!("nodes: {:?}", nodes);

    let children = nodes
        .into_iter()
        .map(|child| view! { <li>"["{child}"]"</li> })
        .collect_view();

    view! {
        <ul>{children}</ul>
    }
}

#[component]
pub fn Sandbox() -> impl IntoView {

    let providers = &["Twitter", "Github", "Discord", "Twitter", "Github", "Discord"];
    let buttons = move || {
        providers
            .iter()
            .map(|provider| {
                view! {
                    <div>
                        {provider.to_string()}
                    </div>
                }
            })
            .collect_view()
    };

    view! {
        <WrapsChildren>
           {buttons}
        </WrapsChildren>

        <WrapsChildren>
           "A"
           "B"
           "C"
        </WrapsChildren>
    }
}

The dynamic content is treated as a single child instead of a list of children (note the surrounding [ ]): image image

gzp79 avatar Apr 18 '24 15:04 gzp79

This is working as designed and is the only reasonable implementation given the way the framework works.

gbj avatar Apr 18 '24 16:04 gbj

In this case how can you create a dynamic number of items where each item is decorated separatelly by the component ?

gzp79 avatar Apr 18 '24 16:04 gzp79

Was putting together this example and got distracted. Rather than using children just pass things as normal props. Here is an example that supports either dynamic or not.

#[component]
pub fn WrapsChildren(#[prop(into)] nodes: MaybeSignal<Vec<View>>) -> impl IntoView {
    let nodes = move || {
        nodes
            .get()
            .into_iter()
            .map(|view| view! { <li>{view}</li> })
            .collect::<Vec<_>>()
    };
    view! {
        <ul>{nodes}</ul>
    }
}

#[component]
pub fn App() -> impl IntoView {
    let providers = &[
        "Twitter", "Github", "Discord", "Twitter", "Github", "Discord",
    ];
    let buttons = move || {
        providers
            .iter()
            .map(|provider| {
                view! {
                    <div>
                        {provider.to_string()}
                    </div>
                }
                .into_view()
            })
            .collect::<Vec<_>>()
    };

    view! {
        <WrapsChildren nodes={Signal::derive(buttons)}/>
        <WrapsChildren nodes={["A", "B", "C"].into_iter().map(IntoView::into_view).collect::<Vec<_>>()}/>
    }
}

gbj avatar Apr 18 '24 16:04 gbj

Just to expand a little for anyone reading this in the future...

This is a genuine drawback of fine-grained reactive UI relative to something like a virtual DOM — Since we only run component functions once to set up the reactive system, the only thing the component function knows about a dynamic child is "this is a dynamic child", because this line only runs once:

    let children = nodes
        .into_iter()

The solution in general is to pass around data, or to pass around more specific data structures. Where the pattern in React might be to compose everything through components and child components, sometimes we'll end up passing props with a little more type information.

There are clear trade-offs here but I think it's worth it.

gbj avatar Apr 19 '24 10:04 gbj