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:
2026-05-16 21:49:33 +02:00
parent 5228a76468
commit 353fe09a99
2 changed files with 26 additions and 12 deletions

View File

@@ -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>

View File

@@ -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>