Skip to main content

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 ✅

ScenarioWhy
Charts below the foldSkeletons show until card enters viewport
Switching to a new tabFresh IntersectionObservers are registered (grid has key={activeTabId})
Revisiting a previous tabdefinitionsCacheRef + loadedRef skip re-fetches
Static/dummy moderesolveChartData runs immediately, no API call
Charts without parametershandleChartVisible fetches data on viewport entry
Drag & resizeLayout-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 id appears 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 / RefRole
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
IntersectionObserverCore mechanism for per-chart lazy load (rootMargin: "100px", threshold: 0.1)
useDashboardLayoutPersists drag order + column widths in localStorage
useLanguageRead 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:

  1. No force-refresh APIloadedRef and definitionsCacheRef cannot be cleared without remounting.
  2. Parameterised charts bypass the IntersectionObserver path entirely — they depend on DynamicParametersForm.
  3. loadedRef is not scoped per-tab — the same chart id on two tabs will only fetch once.