fix(medications): stop the delete confirm hanging in a spinner

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.
This commit is contained in:
Marc-André Bombeck committed 2026-06-01 19:36:33 +02:00
1 parent b3f61d0c75
commit ddb08b048a
2 files changed
+29 -6

No files matched your search

+5
View File
@@ -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
@@ -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);