leptos-rs / leptos

Build fast web applications with Rust.
https://leptos.dev
MIT License
15.31k stars 599 forks source link

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

Closed gzp79 closed 2 months ago

gzp79 commented 2 months ago

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

gbj commented 2 months ago

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

gzp79 commented 2 months ago

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

gbj commented 2 months ago

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 commented 2 months ago

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.