Skip to content

fix: keep an optional event handler alive when a rerender adds it - #5894

Open
XiangpengHao wants to merge 1 commit into
DioxusLabs:mainfrom
XiangpengHao:fix-optional-callback-owner
Open

XiangpengHao wants to merge 1 commit into
DioxusLabs:mainfrom
XiangpengHao:fix-optional-callback-owner

Conversation

@XiangpengHao

Copy link
Copy Markdown

When a memoized component's Option<EventHandler> prop goes from None to Some, memoize moves the new props' handler into the old props (the else branch added in #5225). That handler is owned by the new props' Owner, and the new props are dropped right after the diff, so the component is left holding a freed handler. The next diff of that component panics in Callback::__point_to:

called `Result::unwrap()` on an `Err` value: Dropped(ValueDroppedError { created_at: Location { file: ".../dioxus-core-0.7.10/src/properties.rs", line: 239, column: 14 } })

or with RefCell already borrowed when the freed slot has already been reused for the new handler.

Ordinary code hits it: two same-shaped branches, one without the handler and one with it, which the differ updates in place.

#[component]
fn MaybeClickable(onclick: Option<EventHandler>) -> Element {
    rsx! {
        button { onclick: move |_| if let Some(onclick) = onclick { onclick(()) } }
    }
}

fn app() -> Element {
    if generation() < 3 {
        needs_update();
    }
    if generation() == 0 {
        rsx! { MaybeClickable {} }
    } else {
        rsx! { MaybeClickable { onclick: move |_| {} } }
    }
}

We hit it in an app as a search form's Search button (no onclick) turning into a Stop button (with onclick) while a search runs; the page froze on the next progress update.

Fix

Every other handler and signal field is updated in place, so the old props only ever keep boxes they own. This does the same for an optional handler that is added: the WithOwner wrapper runs the inner memoize under its own owner, and the old props take a new reference to the new handler (Callback::__reference_in_current_owner, built on Owner::insert_reference) instead of moving in the new props' box.

Tests

  • optional_event_handler_diff, the Option<Callback> is always set to Some even if None is explicitly passed #5222 regression test, already diffed None → Some but only asserted is_some(). It now calls the copied callback, which panicked with Dropped before this change.
  • New optional_event_handler_added_on_rerender_survives reproduces it through a component; it panicked before this change.
  • dioxus-core, dioxus-core-macro, dioxus-signals, dioxus-hooks, dioxus-stores, dioxus-ssr and dioxus-router tests pass.

Notes

  • A handler that flips Some → None → Some repeatedly keeps one reference per flip alive until the component unmounts. That matches how the last handler is already kept alive after Some → None; releasing it earlier would break copies the component made of it, as TakesEventHandler in the same test file does.
  • v0.7.10 has the same code and needs the same fix; it cherry-picks cleanly apart from the test, which builds its props with the builder there.

When a memoized component's optional event handler goes from None to Some,
memoize moved the new props' handler into the old props. That handler is
owned by the new props, which are dropped after the diff, so the component
kept a dangling handler and the next diff panicked in __point_to (or with
"RefCell already borrowed" once the freed slot was reused).

The old props now take their own reference to the new handler, owned by
their owner, the way every other handler and signal field is updated in
place.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant