fix(hosts,applications): fix wasm-bindgen panic when closing modal
Closing a modal by clicking the backdrop, Cancel, or × called show_modal.set(false) synchronously inside a wasm-bindgen closure. Leptos immediately unmounts the modal, freeing all its closures while the click handler is still on the call stack, which causes wasm-bindgen to panic with "closure invoked after being dropped". Fix: introduce a close() helper that defers set(false) to the next microtask via spawn_local, so the closure returns to wasm-bindgen before the modal is unmounted. Also switch autofocus from Effect+get() to spawn_local+get_untracked() to avoid subscribing NodeRef as a reactive dependency, which would re-trigger during unmount and risk the same panic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,22 +22,33 @@ fn AddApplicationModal(
|
|||||||
create_action: ServerAction<CreateApplication>,
|
create_action: ServerAction<CreateApplication>,
|
||||||
show_modal: RwSignal<bool>,
|
show_modal: RwSignal<bool>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
use leptos::task::spawn_local;
|
||||||
|
|
||||||
let name_ref = NodeRef::<Input>::new();
|
let name_ref = NodeRef::<Input>::new();
|
||||||
|
|
||||||
// Focus the name field as soon as the modal is mounted.
|
// Defer focus to the next microtask so the element is in the DOM.
|
||||||
Effect::new(move |_| {
|
// Using get_untracked() avoids subscribing to NodeRef's reactive signal,
|
||||||
if let Some(el) = name_ref.get() {
|
// which would otherwise re-trigger during modal unmount and cause
|
||||||
|
// "closure invoked after being dropped" in wasm-bindgen.
|
||||||
|
spawn_local(async move {
|
||||||
|
if let Some(el) = name_ref.get_untracked() {
|
||||||
let _ = el.focus();
|
let _ = el.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// close() defers show_modal.set(false) to the next microtask.
|
||||||
|
// Without this, setting the signal synchronously inside a click handler
|
||||||
|
// unmounts the modal (and frees its closures) while the handler is still
|
||||||
|
// on the call stack, causing wasm-bindgen to panic.
|
||||||
|
let close = move || spawn_local(async move { show_modal.set(false) });
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="modal-backdrop" on:click=move |_| show_modal.set(false)>
|
<div class="modal-backdrop" on:click=move |_| close()>
|
||||||
<div class="modal" on:click=move |e| e.stop_propagation()>
|
<div class="modal" on:click=move |e| e.stop_propagation()>
|
||||||
<div class="modal__header">
|
<div class="modal__header">
|
||||||
<h2>"Add an application"</h2>
|
<h2>"Add an application"</h2>
|
||||||
<button class="modal__close" type="button" aria-label="Close"
|
<button class="modal__close" type="button" aria-label="Close"
|
||||||
on:click=move |_| show_modal.set(false)>
|
on:click=move |_| close()>
|
||||||
"×"
|
"×"
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +69,7 @@ fn AddApplicationModal(
|
|||||||
|
|
||||||
<div class="modal__actions">
|
<div class="modal__actions">
|
||||||
<button class="btn-secondary" type="button"
|
<button class="btn-secondary" type="button"
|
||||||
on:click=move |_| show_modal.set(false)>
|
on:click=move |_| close()>
|
||||||
"Cancel"
|
"Cancel"
|
||||||
</button>
|
</button>
|
||||||
<button type="submit">"Add application"</button>
|
<button type="submit">"Add application"</button>
|
||||||
|
|||||||
@@ -77,17 +77,20 @@ fn AddHostModal(
|
|||||||
networks_res: LocalResource<Result<Vec<crate::models::Network>, ServerFnError>>,
|
networks_res: LocalResource<Result<Vec<crate::models::Network>, ServerFnError>>,
|
||||||
show_modal: RwSignal<bool>,
|
show_modal: RwSignal<bool>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
use leptos::task::spawn_local;
|
||||||
|
|
||||||
let name_ref = NodeRef::<Input>::new();
|
let name_ref = NodeRef::<Input>::new();
|
||||||
|
|
||||||
// Focus the name field as soon as the modal is mounted.
|
spawn_local(async move {
|
||||||
Effect::new(move |_| {
|
if let Some(el) = name_ref.get_untracked() {
|
||||||
if let Some(el) = name_ref.get() {
|
|
||||||
let _ = el.focus();
|
let _ = el.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let close = move || spawn_local(async move { show_modal.set(false) });
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="modal-backdrop" on:click=move |_| show_modal.set(false)>
|
<div class="modal-backdrop" on:click=move |_| close()>
|
||||||
<div class="modal" on:click=move |e| e.stop_propagation()>
|
<div class="modal" on:click=move |e| e.stop_propagation()>
|
||||||
<div class="modal__header">
|
<div class="modal__header">
|
||||||
<h2>"Add a host"</h2>
|
<h2>"Add a host"</h2>
|
||||||
@@ -95,7 +98,7 @@ fn AddHostModal(
|
|||||||
class="modal__close"
|
class="modal__close"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
on:click=move |_| show_modal.set(false)
|
on:click=move |_| close()
|
||||||
>"×"</button>
|
>"×"</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -137,7 +140,7 @@ fn AddHostModal(
|
|||||||
<button
|
<button
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
type="button"
|
type="button"
|
||||||
on:click=move |_| show_modal.set(false)
|
on:click=move |_| close()
|
||||||
>"Cancel"</button>
|
>"Cancel"</button>
|
||||||
<button type="submit">"Add host"</button>
|
<button type="submit">"Add host"</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user