From 1c9a2adf229586a9ec03997e1782d76faa04f9d6 Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Sun, 14 Jun 2026 03:56:49 +0700 Subject: [PATCH 1/2] fix(charts): make data helpers resilient to nullish data Add normalizeChartData() to coerce a chart's data prop to a stable array, and make getDataKeys() null-safe. During streaming, data can be null/undefined before the full payload arrives, which previously threw inside the chart internals. --- .../src/components/Charts/utils/dataUtils.ts | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/components/Charts/utils/dataUtils.ts b/packages/react-ui/src/components/Charts/utils/dataUtils.ts index fe0755e86..e2433a48d 100644 --- a/packages/react-ui/src/components/Charts/utils/dataUtils.ts +++ b/packages/react-ui/src/components/Charts/utils/dataUtils.ts @@ -3,6 +3,27 @@ import { PieChartData } from "../PieChart"; import { RadialChartData } from "../RadialChart"; import { LegendItem } from "../types"; +// Shared empty array reference so that normalizing nullish data does not +// allocate a new array on every render (a fresh `[]` would change identity +// and defeat the charts' `useMemo`/`React.memo` optimizations). +const EMPTY_CHART_DATA: readonly never[] = []; + +/** + * Normalizes a chart's `data` prop to always be an array. + * + * While generative UI is streaming, `data` can momentarily be `null` or + * `undefined` before the full payload arrives. Passing that straight into the + * chart internals throws (e.g. `getDataKeys`, `data.length`, `[...data]`). + * This guard falls back to a stable shared empty array so charts render an + * empty state instead of crashing. + * + * @param data - The raw `data` prop, which may be nullish mid-stream. + * @returns The original array, or a stable empty array when `data` is nullish. + */ +export const normalizeChartData = (data: T | null | undefined): T => { + return (Array.isArray(data) ? data : EMPTY_CHART_DATA) as T; +}; + /** * This function returns the data keys for the chart, used for the data keys of the chart. * @param data - The data to be displayed in the chart. @@ -10,10 +31,10 @@ import { LegendItem } from "../types"; * @returns The data keys for the chart. */ export const getDataKeys = ( - data: Array>, + data: Array> | null | undefined, categoryKey: string, ): string[] => { - return Object.keys(data[0] || {}).filter((key) => key !== categoryKey); + return Object.keys(data?.[0] || {}).filter((key) => key !== categoryKey); }; /** From 2198dd02d2e3626841bc3c8dc6824d90aab2abbe Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Sun, 14 Jun 2026 03:56:49 +0700 Subject: [PATCH 2/2] fix(charts): prevent TypeError crash when streaming nullish data Every public chart component read the data prop directly (getDataKeys, data.length, [...data]) assuming it was always an array. While generative UI streams, data can momentarily be null/undefined, crashing the chart with 'Cannot read properties of null'. Normalize data at each component entry so charts render an empty state instead of throwing. --- .../src/components/Charts/AreaChart/AreaChart.tsx | 3 +++ .../Charts/AreaChartCondensed/AreaChartCondensed.tsx | 3 +++ .../react-ui/src/components/Charts/BarChart/BarChart.tsx | 3 +++ .../Charts/BarChartCondensed/BarChartCondensed.tsx | 3 +++ .../Charts/HorizontalBarChart/HorizontalBarChart.tsx | 3 +++ .../src/components/Charts/LineChart/LineChart.tsx | 3 +++ .../Charts/LineChartCondensed/LineChartCondensed.tsx | 3 +++ .../react-ui/src/components/Charts/PieChart/PieChart.tsx | 4 +++- .../src/components/Charts/RadarChart/RadarChart.tsx | 9 ++++++++- .../src/components/Charts/RadialChart/RadialChart.tsx | 4 +++- .../src/components/Charts/ScatterChart/ScatterChart.tsx | 4 +++- .../SingleStackedBarChart/SingleStackedBarChart.tsx | 3 +++ 12 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx b/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx index d93e92fbb..99c901157 100644 --- a/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx +++ b/packages/react-ui/src/components/Charts/AreaChart/AreaChart.tsx @@ -35,6 +35,7 @@ import { getColorForDataKey, getDataKeys, getLegendItems, + normalizeChartData, } from "../utils/dataUtils"; import { AreaChartData, AreaChartVariant } from "./types"; @@ -83,6 +84,8 @@ const AreaChartComponent = ({ height, width, }: AreaChartProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx b/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx index ea3f13eda..6923795eb 100644 --- a/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx +++ b/packages/react-ui/src/components/Charts/AreaChartCondensed/AreaChartCondensed.tsx @@ -31,6 +31,7 @@ import { getColorForDataKey, getDataKeys, getLegendItems, + normalizeChartData, } from "../utils/dataUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; @@ -79,6 +80,8 @@ const AreaChartCondensedComponent = ({ height = CHART_HEIGHT, width, }: AreaChartCondensedProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx b/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx index cd6225040..4941682e7 100644 --- a/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx +++ b/packages/react-ui/src/components/Charts/BarChart/BarChart.tsx @@ -38,6 +38,7 @@ import { getColorForDataKey, getDataKeys, getLegendItems, + normalizeChartData, } from "../utils/dataUtils"; import { BarChartData, BarChartVariant } from "./types"; import { @@ -97,6 +98,8 @@ const BarChartComponent = ({ height, width, }: BarChartProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx b/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx index 4f5089cba..061c08f0e 100644 --- a/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx +++ b/packages/react-ui/src/components/Charts/BarChartCondensed/BarChartCondensed.tsx @@ -32,6 +32,7 @@ import { getColorForDataKey, getDataKeys, getLegendItems, + normalizeChartData, } from "../utils/dataUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; @@ -93,6 +94,8 @@ const BarChartCondensedComponent = ({ width, maxBarWidth = DEFAULT_MAX_BAR_WIDTH, }: BarChartCondensedProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx b/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx index 568279e19..c462431f2 100644 --- a/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx +++ b/packages/react-ui/src/components/Charts/HorizontalBarChart/HorizontalBarChart.tsx @@ -31,6 +31,7 @@ import { getColorForDataKey, getDataKeys, getLegendItems, + normalizeChartData, } from "../utils/dataUtils"; import { numberTickFormatter } from "../utils/styleUtils"; import { CustomBarShape } from "./components/CustomBarShape"; @@ -90,6 +91,8 @@ const HorizontalBarChartComponent = ({ height, width, }: HorizontalBarChartProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx b/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx index 292dc584c..7df6b4f7e 100644 --- a/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx +++ b/packages/react-ui/src/components/Charts/LineChart/LineChart.tsx @@ -35,6 +35,7 @@ import { getColorForDataKey, getDataKeys, getLegendItems, + normalizeChartData, } from "../utils/dataUtils"; import { LineChartData, LineChartVariant } from "./types"; @@ -83,6 +84,8 @@ export const LineChart = ({ width, strokeWidth = 2, }: LineChartProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/LineChartCondensed/LineChartCondensed.tsx b/packages/react-ui/src/components/Charts/LineChartCondensed/LineChartCondensed.tsx index 33fdbfb8e..1895ab273 100644 --- a/packages/react-ui/src/components/Charts/LineChartCondensed/LineChartCondensed.tsx +++ b/packages/react-ui/src/components/Charts/LineChartCondensed/LineChartCondensed.tsx @@ -31,6 +31,7 @@ import { getColorForDataKey, getDataKeys, getLegendItems, + normalizeChartData, } from "../utils/dataUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; @@ -81,6 +82,8 @@ const LineChartCondensedComponent = ({ width, strokeWidth = 2, }: LineChartCondensedProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx b/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx index da669c8a6..d2bd93682 100644 --- a/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx +++ b/packages/react-ui/src/components/Charts/PieChart/PieChart.tsx @@ -8,7 +8,7 @@ import { useExportChartData, useTransformedKeys } from "../hooks/index.js"; import { DefaultLegend } from "../shared/DefaultLegend/DefaultLegend.js"; import { StackedLegend } from "../shared/StackedLegend/StackedLegend.js"; import { LegendItem } from "../types/Legend.js"; -import { getCategoricalChartConfig } from "../utils/dataUtils.js"; +import { getCategoricalChartConfig, normalizeChartData } from "../utils/dataUtils.js"; import { PaletteName, useChartPalette } from "../utils/PalletUtils.js"; import { PieChartData } from "./types/index.js"; import { @@ -74,6 +74,8 @@ const PieChartComponent = ({ height, width, }: PieChartProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx b/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx index 3eef96795..c81784d5c 100644 --- a/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx +++ b/packages/react-ui/src/components/Charts/RadarChart/RadarChart.tsx @@ -14,7 +14,12 @@ import { useExportChartData, useTransformedKeys } from "../hooks"; import { ActiveDot, CustomTooltipContent, DefaultLegend } from "../shared"; import { LegendItem } from "../types"; import { useChartPalette } from "../utils/PalletUtils"; -import { get2dChartConfig, getDataKeys, getLegendItems } from "../utils/dataUtils"; +import { + get2dChartConfig, + getDataKeys, + getLegendItems, + normalizeChartData, +} from "../utils/dataUtils"; import { AxisLabel } from "./components/AxisLabel"; import { RadarChartData } from "./types"; @@ -52,6 +57,8 @@ const RadarChartComponent = ({ height, width, }: RadarChartProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx b/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx index 7e17dffbc..f1374e694 100644 --- a/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx +++ b/packages/react-ui/src/components/Charts/RadialChart/RadialChart.tsx @@ -7,7 +7,7 @@ import { useExportChartData, useTransformedKeys } from "../hooks"; import { DefaultLegend } from "../shared/DefaultLegend/DefaultLegend"; import { StackedLegend } from "../shared/StackedLegend/StackedLegend"; import { LegendItem } from "../types/Legend"; -import { getCategoricalChartConfig } from "../utils/dataUtils"; +import { getCategoricalChartConfig, normalizeChartData } from "../utils/dataUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { RadialChartData } from "./types"; import { @@ -68,6 +68,8 @@ export const RadialChart = ({ height, width, }: RadialChartProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx b/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx index eb4ad70ea..98e032cba 100644 --- a/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx +++ b/packages/react-ui/src/components/Charts/ScatterChart/ScatterChart.tsx @@ -14,7 +14,7 @@ import { YAxisTick, } from "../shared"; import { LegendItem } from "../types"; -import { get2dChartConfig, getLegendItems } from "../utils/dataUtils"; +import { get2dChartConfig, getLegendItems, normalizeChartData } from "../utils/dataUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { numberTickFormatter } from "../utils/styleUtils"; import ScatterDot from "./components/ScatterDot"; @@ -61,6 +61,8 @@ export const ScatterChart = ({ width, shape = "circle", }: ScatterChartProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const printContext = usePrintContext(); isAnimationActive = printContext ? false : isAnimationActive; diff --git a/packages/react-ui/src/components/Charts/SingleStackedBarChart/SingleStackedBarChart.tsx b/packages/react-ui/src/components/Charts/SingleStackedBarChart/SingleStackedBarChart.tsx index c06918548..ee7980e8a 100644 --- a/packages/react-ui/src/components/Charts/SingleStackedBarChart/SingleStackedBarChart.tsx +++ b/packages/react-ui/src/components/Charts/SingleStackedBarChart/SingleStackedBarChart.tsx @@ -7,6 +7,7 @@ import { DefaultLegend } from "../shared/DefaultLegend/DefaultLegend"; import { FloatingUIPortal } from "../shared/PortalTooltip"; import { StackedLegend } from "../shared/StackedLegend/StackedLegend"; import { LegendItem, StackedLegendItem } from "../types"; +import { normalizeChartData } from "../utils/dataUtils"; import { PaletteName, useChartPalette } from "../utils/PalletUtils"; import { ToolTip } from "./components"; import { SingleStackedBarData } from "./types"; @@ -36,6 +37,8 @@ export const SingleStackedBar = ({ style, animated = true, }: SingleStackedBarProps) => { + // Guard against nullish `data` while generative UI is streaming. + data = normalizeChartData(data); const [isLegendExpanded, setIsLegendExpanded] = useState(false); const [activeIndex, setActiveIndex] = useState(null); const [hoveredLegendKey, setHoveredLegendKey] = useState(null);