GenericDashboard — Lazy Loading
GenericDashboard uses two independent lazy-loading layers to keep the network quiet until the user actually needs data.
Level 1 — Tab definitions (fetchChartDefinitions)
Triggered when a user clicks a tab for the first time:
User clicks tab → handleTabChange fires
→ if not in definitionsCacheRef → fetchChartDefinitions(tabId)
→ result cached in definitionsCacheRef (a plain useRef)
→ setDynamicDefsMap({ [tabId]: defs })
definitionsCacheRef is a ref (not state), so revisiting a tab never re-fetches definitions for the entire session lifetime.
Level 2 — Chart data (IntersectionObserver inside LazyChart)
Each chart card is wrapped in a LazyChart component that uses an IntersectionObserver:
LazyChart mounts → visible = false → renders <Skeleton>
→ card enters viewport (+100px margin, 10% threshold)
→ setVisible(true) → ReportChart renders
→ onVisible(def.id) fires → handleChartVisible(id)
→ if not in loadedRef (Set) → fetchChartData(def.name, [])
→ setChartDataMap / setLoadingMap updated
loadedRef (a useRef<Set<number>>) ensures each chart is fetched exactly once per mount.
Where it works ✅
| Scenario | Why |
|---|---|
| Charts below the fold | Skeletons show until card enters viewport |
| Switching to a new tab | Fresh IntersectionObservers are registered (grid has key={activeTabId}) |
| Revisiting a previous tab | definitionsCacheRef + loadedRef skip re-fetches |
| Static/dummy mode | resolveChartData runs immediately, no API call |
| Charts without parameters | handleChartVisible fetches data on viewport entry |
| Drag & resize | Layout-only changes; loaded state is unaffected |
Known limitations ❌
1. Charts WITH parameters bypass the IntersectionObserver
if (def.parameters?.length) return; // hard skip
These charts rely on DynamicParametersForm's autoSubmit. If autoSubmit fails, the chart shows "Please apply filters to view the chart" indefinitely.
2. loadedRef is never cleared
It lives for the entire GenericDashboard lifetime:
- Calling
resetLayout()resets visual layout but does not re-fetch data. - If the same chart
idappears on two different tabs, the second tab's chart never fetches. - There is no public API to force-refresh — you must fully remount
GenericDashboard.
3. DynamicParametersForm auto-submit race condition
For parameterised charts, autoSubmit is set to !hasAutoSubmitted. If ReportChart re-renders before the auto-submit effect fires (e.g. a language toggle during load), a double submit can occur.
4. Tab panels mount all content simultaneously
DashboardTabs uses CSS display: none to hide inactive tabs — all tab panels are in the DOM at once. This is harmless only because tab.content is null in GenericDashboard. If you pass real content via tab.content, it will all mount immediately.
Key hooks & refs
| Hook / Ref | Role |
|---|---|
loadedRef (useRef<Set<number>>) | Deduplication guard for chart data fetches |
definitionsCacheRef (useRef<Record>) | Deduplication guard for tab definition fetches |
activeChartDefsRef (useRef) | Lets stale callbacks read latest chart definitions without re-subscribing |
IntersectionObserver | Core mechanism for per-chart lazy load (rootMargin: "100px", threshold: 0.1) |
useDashboardLayout | Persists drag order + column widths in localStorage |
useLanguage | Read inside LazyChart to avoid stale closures in IntersectionObserver callbacks |
Key takeaway
Lazy loading works well for the standard "no-parameter charts in a scrollable grid" case. The main gaps are:
- No force-refresh API —
loadedRefanddefinitionsCacheRefcannot be cleared without remounting. - Parameterised charts bypass the IntersectionObserver path entirely — they depend on
DynamicParametersForm. loadedRefis not scoped per-tab — the same chartidon two tabs will only fetch once.