From 6cd8ffa52614c33fef55a7f53182e59d3cd719ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 11:32:57 +0200 Subject: [PATCH] fix(documents): follow the address bar for the open document The sheet compared useSearchParams with the last ?doc= it had seen. Next 16.4 applies its copy of a pushState after the write, and a Back pressed in between leaves useSearchParams reading no document on both sides, so the sheet saw no change and stayed open. The sheet now reads the query from window.location through useSyncExternalStore, re-read on popstate and on every render, and the selection and close paths build their URLs from the same live value. --- src/components/documents/documents-view.tsx | 44 ++++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/components/documents/documents-view.tsx b/src/components/documents/documents-view.tsx index 187ce48a0..ffbb2e535 100644 --- a/src/components/documents/documents-view.tsx +++ b/src/components/documents/documents-view.tsx @@ -31,7 +31,14 @@ import { } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from "react"; import { toast } from "sonner"; import { showErrorToast } from "@/lib/errors/error-toast"; @@ -110,6 +117,26 @@ const BULK_VISIT_OPTIONS = 12; const DOCUMENT_QUERY_ID_PATTERN = /^[a-zA-Z0-9_-]{1,40}(?![\s\S])/; const DOCUMENT_SELECTION_HISTORY_KEY = "__healthlogPushedDocumentSelection"; +/** + * The query string in the address bar, read at render time. + * + * The open sheet follows `?doc=`, and `useSearchParams` is not a faithful + * copy of it: Next 16.4 applies the copy of a `pushState` later than the write, + * and a Back pressed in between moves the address bar from `?doc=` to the + * page without it while `useSearchParams` can read "no document" on both sides. + * The sheet then saw no change and stayed open. `window.location` is never + * behind; `popstate` re-reads it on every traversal, and every other render + * (including the ones Next's own navigations cause) reads it afresh. + */ +function subscribeToTraversal(onChange: () => void): () => void { + window.addEventListener("popstate", onChange); + return () => window.removeEventListener("popstate", onChange); +} + +function readLocationSearch(): string { + return window.location.search; +} + export function documentSelectionHistoryState( documentId: string, ): Record { @@ -818,7 +845,13 @@ export function DocumentsView() { const [detailOpen, setDetailOpen] = useState(false); const handedOffDocumentRef = useRef(null); - const rawDocParam = searchParams.get("doc"); + // Before hydration the server's view of the query stands in for the URL. + const locationSearch = useSyncExternalStore( + subscribeToTraversal, + readLocationSearch, + () => searchParams.toString(), + ); + const rawDocParam = new URLSearchParams(locationSearch).get("doc"); const docParam = rawDocParam !== null && DOCUMENT_QUERY_ID_PATTERN.test(rawDocParam) ? rawDocParam @@ -837,14 +870,14 @@ export function DocumentsView() { const openDetail = useCallback( (id: string) => { - const href = documentSelectionHref(pathname, searchParams.toString(), id); + const href = documentSelectionHref(pathname, window.location.search, id); if (href === null) return; handedOffDocumentRef.current = null; setDetailId(id); setDetailOpen(true); window.history.pushState(documentSelectionHistoryState(id), "", href); }, - [pathname, searchParams], + [pathname], ); const handleDetailOpenChange = useCallback((open: boolean) => { @@ -881,7 +914,7 @@ export function DocumentsView() { handedOffDocumentRef.current = closeDocumentSelectionAfterCoachHandoff( window.history, pathname, - searchParams.toString(), + window.location.search, docParam, handedOffDocumentRef.current, coachLaunch?.closeIntent, @@ -925,7 +958,6 @@ export function DocumentsView() { docParam, moduleEnabled, pathname, - searchParams, ]); // Hover/focus intent prefetches the detail METADATA (never the blob —