From 03020124418042e8619c9de6dd241b00e7ea544f Mon Sep 17 00:00:00 2001 From: delabrcd Date: Mon, 22 Jun 2026 18:25:48 +0000 Subject: [PATCH] fix(widgets): guard IntervalHeatmap children against null payload The #150 refactor onto useIntervalPayload + IntervalWidgetBody left the heatmap's chart subtree dereferencing payload!.grid / payload!.rowLabels. React evaluates the children passed to IntervalWidgetBody eagerly, BEFORE the wrapper decides to render the loading skeleton, so during the initial loading tick (payload === null) this threw an unguarded render-time TypeError that took the whole dashboard down ("Application error: a client-side exception has occurred"), not just the widget. Wrap the populated subtree in {payload && ( ... )} and drop the non-null assertions so payload is dereferenced only when present; while loading/ error/empty, children is falsy and IntervalWidgetBody shows its skeleton/empty as intended. Behavior otherwise identical. Audited the two sibling widgets the #150 refactor touched: IntervalLoadShape dereferences payload only inside guarded derivations (!state || 'error' in state) so its eager children are safe; IntervalHistory keeps its own SWR state and was not moved onto the hook, also safe. --- .../components/widgets/IntervalHeatmap.tsx | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/app/src/components/widgets/IntervalHeatmap.tsx b/app/src/components/widgets/IntervalHeatmap.tsx index 684d240..802a7fc 100644 --- a/app/src/components/widgets/IntervalHeatmap.tsx +++ b/app/src/components/widgets/IntervalHeatmap.tsx @@ -125,26 +125,35 @@ export function IntervalHeatmap({ } > - <> - {/* Peak-demand readout caption (#77): value + when. Hidden if no peak. */} - {peakReadout && ( -
- {peakReadout} + {/* #150 regression fix: `children` is evaluated EAGERLY by React when + IntervalHeatmap renders — BEFORE IntervalWidgetBody decides to show the + loading skeleton. During the loading tick useIntervalPayload returns + payload === null, so dereferencing payload.grid here would throw an + unguarded render-time TypeError and take the WHOLE dashboard down. Gate + the populated subtree on `payload &&` so children is falsy until the + payload exists; IntervalWidgetBody renders its skeleton/empty meanwhile. */} + {payload && ( + <> + {/* Peak-demand readout caption (#77): value + when. Hidden if no peak. */} + {peakReadout && ( +
+ {peakReadout} +
+ )} + {/* The grid fills the remaining height. HeatmapViz draws a scalable SVG + inside a box of the height we pass; in the fill cell we let it take + the flex remainder via a min-h-0 flex-1 wrapper. We pass the + SERVER-computed grid + rowLabels (no client re-aggregation). */} +
+
- )} - {/* The grid fills the remaining height. HeatmapViz draws a scalable SVG - inside a box of the height we pass; in the fill cell we let it take - the flex remainder via a min-h-0 flex-1 wrapper. We pass the - SERVER-computed grid + rowLabels (no client re-aggregation). */} -
- -
- + + )} ); };