From bc9e8ef62f97f18187bef56e0b159d55a9de2a5f Mon Sep 17 00:00:00 2001 From: marobinette Date: Fri, 2 Oct 2026 10:52:39 -0400 Subject: [PATCH 1/3] FIX: Wait for saved profile before fetching topic reports --- .../reports-by-topic/ReportsByTopic.tsx | 11 +++++++++-- frontend/src/components/profile/profileStore.ts | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx b/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx index 11f84c03..bb76aef4 100644 --- a/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx +++ b/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx @@ -17,7 +17,11 @@ import { Text, Title, } from '@mantine/core'; -import { useProfile, Location } from '@/components/profile/profileStore'; +import { + useProfile, + useProfileHydrated, + Location, +} from '@/components/profile/profileStore'; import { BASE_API_URL } from '@/config'; import { DemographicsDashboard, @@ -551,6 +555,8 @@ export default function ReportsByTopic({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isExporting, setIsExporting] = useState(false); + // Skip the hydration render's default locations (see useProfileHydrated). + const hydrated = useProfileHydrated(); // --------------------------------------------------------------------------- // Fetch both locations whenever section or location names change — plus, @@ -560,6 +566,7 @@ export default function ReportsByTopic({ // section, since it's supplementary to the primary table. // --------------------------------------------------------------------------- useEffect(() => { + if (!hydrated) return; const cfg = SECTIONS[section]; // eslint-disable-next-line react-hooks/set-state-in-effect -- fetch effect: reset status before the async request setLoading(true); @@ -636,7 +643,7 @@ export default function ReportsByTopic({ // .name changes whenever type/county/town does (it's derived from them), // so it's a reliable proxy for "the location changed" without needing // the whole objects in the dependency array. - }, [section, myLocation.name, comparison.name]); // eslint-disable-line react-hooks/exhaustive-deps + }, [hydrated, section, myLocation.name, comparison.name]); // eslint-disable-line react-hooks/exhaustive-deps // --------------------------------------------------------------------------- // Section-wide data (cfg.allAreas) doesn't depend on the locations, so it's diff --git a/frontend/src/components/profile/profileStore.ts b/frontend/src/components/profile/profileStore.ts index 582d2de5..09160be9 100644 --- a/frontend/src/components/profile/profileStore.ts +++ b/frontend/src/components/profile/profileStore.ts @@ -1,3 +1,4 @@ +import { useSyncExternalStore } from 'react'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; @@ -76,3 +77,17 @@ export const useProfile = create()( }, ), ); + +// Whether the saved profile has loaded from localStorage. Pages are statically +// exported, so on a full page load React's hydration render gets the store's +// defaults (Vermont vs. Vermont) from useProfile(), and the saved profile only +// arrives on the next render. Wait for this before fetching by location or +// seeding local state from the profile. Client-side navigation is already +// hydrated, so this is true from the first render there. +export function useProfileHydrated(): boolean { + return useSyncExternalStore( + (onChange) => useProfile.persist.onFinishHydration(onChange), + () => useProfile.persist.hasHydrated(), + () => false, + ); +} From 0796c1e060cab19342ef7df345f8495a832c60f3 Mon Sep 17 00:00:00 2001 From: marobinette Date: Fri, 9 Oct 2026 09:48:34 -0400 Subject: [PATCH 2/3] remove stale comment --- .../app/data-comparison/reports-by-topic/ReportsByTopic.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx b/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx index bb76aef4..e50c6dd0 100644 --- a/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx +++ b/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx @@ -597,9 +597,6 @@ export default function ReportsByTopic({ }; const timeseriesKeys = Object.keys(cfg.timeseries ?? {}); - // Ignore a response once the locations change again: the first fetch - // (made before the saved profile loads) can otherwise land last and - // show the default places' numbers under the profile's names. let cancelled = false; Promise.all([ From 2243a9341d781f94b63bc220795621576816ebc3 Mon Sep 17 00:00:00 2001 From: marobinette Date: Fri, 9 Oct 2026 10:22:25 -0400 Subject: [PATCH 3/3] remove unused code --- .../src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx b/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx index d06b12fe..2072b77e 100644 --- a/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx +++ b/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx @@ -20,7 +20,6 @@ import { useProfileHydrated, Location, } from '@/components/profile/profileStore'; -import { BASE_API_URL } from '@/config'; import { DemographicsDashboard, LandUseDashboard, @@ -196,7 +195,6 @@ export default function ReportsByTopic({ const [exporting, setExporting] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [isExporting, setIsExporting] = useState(false); // Skip the hydration render's default locations (see useProfileHydrated). const hydrated = useProfileHydrated();