leptos-rs / leptos

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

Differing behaviour between dev and release compilations when updating/rendering RwSignal<Vec<(Fragment, i32)>> #1223

Open MinaMatta98 opened 1 year ago

MinaMatta98 commented 1 year ago

Describe the bug Hello,

I am currently working on a demonstrative chatting client using leptos.

I have an RwSignal<Vec<(Fragment, i32)>> (Don't judge me), being updated via the following

                                                    val.push((
                                                    view!{cx,
                                                        <>
                                                            <div class="flex mt-2 gap-x-3 text-sm border-gray-300 rounded-md bg-sky-200 p-2 w-fit" id=item.id>
                                                                {link.inner_text()}
                                                                <Icon icon=IoIcon::IoClose class="h-3 w-3" on:click=move |_| {
                                                                    input_signal.update(|val| {
                                                                        let index = val.iter().position(|(_, id)| *id == link.value()).unwrap();
                                                                        val.remove(index);
                                                                    });
                                                                    (!input_signal.get().iter().any(|(_, id)| *id == link.value())).then(|| {
                                                                        input.set_value(&(input.value().replace(&(link.value().to_string() + ","), "")));
                                                                        input.set_value(&(input.value().replace(&(link.value().to_string()), "")));
                                                                    });
                                                                }/>
                                                            </div>
                                                        </>
                                                    },item.id)

And rendered here:

              <input type="text" class=move || format!("w-full py-2 px-4 border border-gray-300 rounded-md
                focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500
                text-transparent select-none selection:bg-none {}", if disabled.get() {"opacity-50"} else {""})
                placeholder="Select an option"
                name="other_users"
                _ref=_ref
                on:click=move |_| hidden_state.update(|val| *val = !*val )/>
                             {move || input_signal.get().iter().map(|val| {
                                  val.0.clone() // <--- Here
                              }).collect_view(cx)}
              <ul class=move || format!("absolute z-10 mt-1 w-full bg-white border border-gray-300 rounded-md shadow-lg {}", if hidden_state.get() {"hidden"} else {"block"})>

Leptos Dependencies

As requested:

actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros", "secure-cookies"] }
actix-identity = { version = "0.5.2", optional = true }
actix-session = { version = "0.7.2", features = ["redis", "redis-rs-session"], optional = true }
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { version = "0.3", default-features = false, features = [
  "serde",
] }
leptos_meta = { version = "0.3", default-features = false }
leptos_actix = { version = "0.3", optional = true }
leptos_router = { version = "0.3", default-features = false }
wasm-bindgen = "0.2.87"
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96"
getrandom = { version = "0.2.9", features = ["js"] }
serde_urlencoded = "0.7.1"
fancy-regex = { version = "0.11.0" }
validator = { version = "0.16.0", features = ["derive", "phone"] }
lazy_static = "1.4.0"
env_logger = "0.10.0"
web-sys = { version = "0.3.63", features = ["HtmlFormElement", "SubmitEvent", "KeyboardEvent", "Window", "Location", "History", "File", "FileList", "HtmlInputElement", "HtmlLiElement"] }
gloo-net = "0.2.6"
gloo-file = { version = "0.2.3", features = ["futures"] }
tokio = { version = "1.28.1", features = ["rt", "process"], optional = true }
wasm-bindgen-futures = "0.4.36"
sea-orm-migration = { version = "0.11.3"}
async-trait = "0.1.68"
sea-orm = { version = "0.11.3", features = ["sqlx-mysql", "runtime-tokio-native-tls", "with-chrono"], optional = true }
lettre = { version = "0.10.4", optional = true }
askama = "0.12.0"
base64 = "0.21.2"
chrono = "0.4.24"
rand = "0.8.5"
redis = "0.23.0"
argon2 = "0.5.0"
leptos_icons = { version = "0.0.12", default_features = false ,features = ["AiCloseCircleFilled", "HiChatBubbleOvalLeftEllipsisSolidMd", "HiUserCircleSolidMd", "HiArrowLeftCircleOutlineLg", "AiUserOutlined", "AiUserAddOutlined", "HiChevronLeftSolidLg", "BiUserCircleSolid", "HiEllipsisHorizontalSolidMd", "TbPhotoFilled", "HiPaperAirplaneOutlineLg", "LuImageOff", "IoClose", "IoTrash", "FiAlertTriangle"] }
futures-util = "0.3.28"
iter_tools = "0.1.4"
infer = "0.13.0"

To Reproduce Steps to reproduce the behavior:

  1. Clone the following link
  2. Run docker build -t zing .. This may take 10-15 minutes to build depending on your system.
  3. Run docker run -p 8000:8000 zing
  4. Create 3 accounts with valid email addresses as you will need to validate your email.
  5. Visit this link and click the group chat icon adjacent to the "messages" header.
  6. Select the other two users you have created and click on create. You will need to reload after this as I have not yet created an appropriate update signal.

Expected behavior The expected behavior is that within the first image (Dev build).

The unexpected behavior is that within the second image (release build).

Screenshots image image

Additional context The component is the GroupChatModal on line 1295 of the following link.

If I fix this issue, I will post the fix.

gbj commented 1 year ago

Thanks for this very detailed report!

The issue almost certainly has to do with the way you are cloning and reusing the fragment, I agree.

I'd recommend trying two things: 1) Is it possible for this to be an RwSignal<Vec<(HtmlElement<Div>, i32)>> instead? i.e., to simply drop the wrapping <></> fragment around the <div> you are using? If so, this would likely work correctly. 2) It's worth checking whether this is an issue that's been fixed between 0.3 and the current main release by updating from 0.3 to a dependency on git main.

Otherwise, a minimal reproduction that doesn't require a 10-15 minute build process, creating user accounts, etc. to set up will make it more likely I'll get to this sooner.

MinaMatta98 commented 1 year ago

Ah, I see.

Is there a difference between the implementation of Fragment and other view elements at compile?

gbj commented 1 year ago

Yes. Fragments are backed by a regular web DocumentFragment, which are somewhat tricky to work with. (Although honestly, all DOM elements are hard to deal with). Appending a DocumentFragment to the DOM somewhere moves its children out, so we have to gather them back in if you unmount it and move them somewhere else. It's possible there's an edge case that's missed here somewhere, and the discrepancy between debug and release mode suggests that's the case, but it's also just true that fragments are trickier than regular elements.

MinaMatta98 commented 1 year ago

Ok,

I have found a solution, but it points to an issue within the intricacies within the implementations of collect_view(cx).

A slight modification to the initial implementation has been introduced from:

{ move ||
    input_signal.get().iter().map(|val| {
        val.0
    })
.collect_view(cx)}

to:

                     <For
                       each=move || input_signal.get()
                       key=|input| input.1
                       view=move |cx, item: (HtmlElement<Div>, i32)| {
                         view! {
                           cx,
                             {item.0}
                          }}/>

seems to have fixed the issue.

gbj commented 1 year ago

Here's a simpler reproduction, for my own purposes in addressing this one:

#[component]
fn App(cx: Scope) -> impl IntoView {
    let count = create_rw_signal(cx, 0);
    let frags = create_rw_signal(cx, vec![]);

    view! { cx,
        <button on:click=move |_| {
            count.update(|n| *n += 1);
            frags.update(|f| f.push((
                view! { cx, <><span>{count.get()}</span></> },
                count.get()
            )));
        }>"+1"</button>
        <div>
            {move || frags.get().iter().map(|val| {
                val.0.clone() // <--- Here
            }).collect_view(cx)}
        </div>
    }
}