dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

Infinite loop when wasm-bindgen Closures are used to alter signal state

Open s1gtrap opened this issue 1 year ago • 0 comments

Problem

poll_tasks of virtual_dom.rs is entering an infinite loop when wasm-bindgen Closures are used. Initially discovered when I tried to propagate changes to a signal from a wasm_bindgen::closure::Closure. Specifically to be used with the Ace editor, but any other EventListener registered with add_event_listener_with_callback seems to cause the same issue.

Not sure how useful this would be, but after finding the culprit with the profiler (99% of runtime is spent in poll_tasks), I added some debug prints in trying to figure out what was happening. In my experience, the loop on line 431 is entered, and despite receiving TaskNotified continually and calling handle_task_wakeup, the halting condition is never met as self.dirty_scopes is never populated and remains empty.

Relevant discussion: https://discord.com/channels/899851952891002890/1207014268994592818

Steps To Reproduce

Steps to reproduce the behavior:

I came up with this minimal example:

#![allow(non_snake_case)]

use dioxus::prelude::*;
use wasm_bindgen::prelude::*;

#[component]
pub fn Editor(state: Signal<()>) -> Element {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let mut editor = use_signal(|| None);
    use_effect({
        move || {
            log::info!("enter 1st effect");
            if editor.read().is_none() {
                let elem = document.get_element_by_id("editor").unwrap();
                let closure = Closure::new({
                    move || {
                        log::info!("enter event handler");
                        *state.write() = ();
                        log::info!("exit event handler");
                    }
                });
                elem.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())
                    .unwrap();
                closure.forget();
                *editor.write() = Some(elem);
            }
            log::warn!("exit 1st effect");
        }
    });
    use_effect(move || {
        log::info!("enter 2nd effect");
        if let Some(editor) = &*editor.read() {
            editor.set_inner_html(&format!("{:?}", (&state))); // THIS
        }
        log::warn!("exit 2nd effect");
    });
    rsx! {
        div {
            id: "editor",
            class: "h-full bg-slate-100",
            ""
        }
    }
}

fn app() -> Element {
    let state = use_signal(|| ());
    rsx! {
        div {
            div {
                class: "h-screen",
                Editor {
                    state,
                }
            }
        }
    }
}

fn main() {
    dioxus_logger::init(log::LevelFilter::Trace).expect("failed to init logger");
    console_error_panic_hook::set_once();
    tracing_wasm::set_as_global_default_with_config(
        tracing_wasm::WASMLayerConfigBuilder::default()
            .set_max_level(tracing::Level::TRACE)
            .build(),
    );
    launch(app);
}

which consists of two effects, one to initialize the web_sys::Element with get_element_by_id and the other to register events after. The loop can be incurred by clicking the element, and after a couple seconds your browser will complain/warn you about a script not responding. Interestingly if you comment the line marked with // THIS the loop doesn't occur, presumably because the 2nd effect no longer depends on the state signal which seems to trigger the endless loop.

Expected behavior

To not cause the page to stop responding.

Screenshots

If applicable, add screenshots to help explain your problem.

Environment:

  • Dioxus version: master
  • Rust version: 1.78.0, nightly
  • OS info: MacOS
  • App platform: web`

Questionnaire

  • [ ] I'm interested in fixing this myself but don't know where to start
  • [ ] I would like to fix and I have a solution
  • [ ] I don't have time to fix this right now, but maybe later

s1gtrap avatar Feb 17 '24 15:02 s1gtrap