Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import {
Stack,
Text,
} from '@mantine/core';
import { useProfile, Location } from '@/components/profile/profileStore';
import {
useProfile,
useProfileHydrated,
Location,
} from '@/components/profile/profileStore';
import {
DemographicsDashboard,
LandUseDashboard,
Expand Down Expand Up @@ -191,6 +195,8 @@ export default function ReportsByTopic({
const [exporting, setExporting] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Skip the hydration render's default locations (see useProfileHydrated).
const hydrated = useProfileHydrated();

// ---------------------------------------------------------------------------
// Fetch both locations whenever section or location names change — plus,
Expand All @@ -200,6 +206,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);
Expand All @@ -212,9 +219,6 @@ export default function ReportsByTopic({
) => postSection(cfg, url, location, endpoint);

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([
Expand Down Expand Up @@ -258,7 +262,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
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/components/profile/profileStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useSyncExternalStore } from 'react';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

Expand Down Expand Up @@ -76,3 +77,17 @@ export const useProfile = create<ProfileStore>()(
},
),
);

// 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,
);
}
Loading