From ddb08b048ac84af4a19d84157a2a39043a87ba78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 1 Jun 2026 19:36:33 +0200 Subject: [PATCH] fix(medications): stop the delete confirm hanging in a spinner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleting a medication from the danger zone awaited a prefix-invalidate of the "medications" key bundle before navigating. That bundle covers the detail page's own active query, so the invalidation refetched the just- deleted resource, hit a 404, retried with backoff, and never resolved — the confirm button spun forever even though the delete had persisted. Evict the detail query before invalidating, navigate to the list first, then fire-and-forget the dependent-bundle invalidation. The busy state is also cleared on the failed-response path. The detail query opts out of retries so a deleted-resource refetch never burns a backoff cycle. --- src/app/medications/[id]/page.tsx | 5 ++++ .../sections/destructive-zone-section.tsx | 30 +++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/app/medications/[id]/page.tsx b/src/app/medications/[id]/page.tsx index 4ce75cc2a..4f5a5aa93 100644 --- a/src/app/medications/[id]/page.tsx +++ b/src/app/medications/[id]/page.tsx @@ -132,6 +132,11 @@ export default function MedicationDetailPage({ return (await res.json()).data as MedicationDetailSnapshot; }, enabled: isAuthenticated, + // A deleted medication 404s here. Don't burn a retry/backoff cycle on + // a resource that no longer exists — the delete handler evicts this + // key on success, but `retry: false` hardens any caller that + // prefix-invalidates `["medications"]` while this page is mounted. + retry: false, }); // v1.7.2 W3 — derive the wizard payload purely to feed the read-only diff --git a/src/components/medications/sections/destructive-zone-section.tsx b/src/components/medications/sections/destructive-zone-section.tsx index 16888ed9a..29d617ed0 100644 --- a/src/components/medications/sections/destructive-zone-section.tsx +++ b/src/components/medications/sections/destructive-zone-section.tsx @@ -47,7 +47,11 @@ import { Card } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { useTranslations } from "@/lib/i18n/context"; -import { invalidateKeys, medicationDependentKeys } from "@/lib/query-keys"; +import { + invalidateKeys, + medicationDependentKeys, + queryKeys, +} from "@/lib/query-keys"; const PAUSE_SWITCH_ID = "medication-detail-pause-switch"; const PAUSE_TITLE_ID = "medication-detail-pause-title"; @@ -283,15 +287,29 @@ export function DangerZoneBody({ }); if (!res.ok) { toast.error(t("medications.detail.zone.delete.failed")); + setTier3bBusy(false); return; } - // Cache cascade first so the list page paints without the - // medication, then route. The bundle includes - // `compliance-chart-inline` so the inline chart's stale cache - // evicts before the user lands on the list. - await invalidateKeys(queryClient, medicationDependentKeys); + // Drop the deleted medication's own detail query before the + // prefix-invalidate below. `medicationDependentKeys` carries + // `["medications"]`, which covers the active `["medications", id]` + // detail query on this page; awaiting that invalidation refetched + // a now-404 resource, retried, and hung the spinner forever (the + // delete had already persisted server-side). `removeQueries` + // evicts the dead key so the prefix-invalidate can't refetch it. + queryClient.removeQueries({ + queryKey: queryKeys.medicationDetail(medicationId), + }); + // Navigate away first — we're leaving the detail page, so the + // dependent-bundle invalidation can settle in the background while + // the list page mounts and re-reads `["medications"]` fresh. toast.success(t("medications.detail.zone.delete.toast")); + setDeleteDialogOpen(false); router.push("/medications"); + // Fire-and-forget the dependent-bundle invalidation (list / + // analytics / compliance / inline chart). Not awaited: navigation + // already happened and a transient refetch must not block it. + void invalidateKeys(queryClient, medicationDependentKeys); } catch { toast.error(t("medications.detail.zone.delete.failed")); setTier3bBusy(false);