Recharts 图表在 flex 卡片里变白块:ResponsiveContainer 的宽度塌陷陷阱
在组件库的状态展示页里,我们把三张 AreaChart 卡片并排放入一个 flex items-center justify-center 的容器。同一个图表组件在"预览"Tab 宽屏展示时渲染正常,切到卡片 Gallery 后三格全是空白方块——没有报错、没有数据异常、SVG 节点存在,就是什么都不画。
第一反应是数据没传下去,或者动画时序问题,折腾了一圈才发现:这是纯粹的 CSS 布局尺寸 bug。
现象
- 同一个
<ResponsiveContainer width="100%" height={280}>组件,宽容器里正常,flex 卡片里空白。 - 浏览器控制台出现:
The width(0) and height(0) of chart should be greater than 0, please check the style of container, or add an explicit width and height to the chart. - jsdom 单元测试里
container.querySelector("svg")永远返回null,SVG 从未渲染。
根因
ResponsiveContainer 通过 ResizeObserver 量自己的父元素来决定图表宽高。关键链条是:
- 父元素是
flex items-center justify-center(或inline-flex、w-fit)——这类容器是收缩适配(shrink-to-fit),自身宽度由子内容决定。 - 图表 wrapper 写了
w-full(即width: 100%)——百分比宽度向上查找有明确宽度的包含块才能解析,收缩容器没有明确宽度,解析结果是 0。 - ResponsiveContainer 量到宽度 0,SVG viewport 为
0×280,所有绘制路径都被裁剪掉,画面空白。
症状极具迷惑性:宽容器里 w-full 有效(父有明确宽),卡片里 w-full 失效(父收缩),导致看起来像数据或渲染条件问题,实际只是尺寸计算问题。
width: 100%(Tailwind w-full)依赖包含块有明确宽度。把它放在 flex items-center justify-center / inline-flex / w-fit 的子项里,宽度解析结果是 0——ResponsiveContainer 量到的父宽也是 0,图表不渲染。
正解
给图表 wrapper 换成显式固定宽度,让 ResponsiveContainer 有真实数值可量,同时用 max-w-full 保持窄屏响应性。
// ❌ 在收缩 flex 容器里,w-full → 宽度 0,图表空白
<div className="w-full max-w-xl" style={{ height: 280 }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}>…</AreaChart>
</ResponsiveContainer>
</div>
// ✅ 固定宽度给出可量的基准;max-w-full 防止窄屏溢出
<div className="w-[32rem] max-w-full" style={{ height: 280 }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}>…</AreaChart>
</ResponsiveContainer>
</div>如果你能修改父容器:给它加 flex: 1 1 auto + min-width: 0,或者直接停止收缩布局。但改图表 wrapper 更可移植——不影响共享布局里的其他组件。
w-[32rem] max-w-full:固定宽给 ResizeObserver 一个真实基准,max-w-full 保证在窄父容器里不溢出。height 本就需要显式设置,这一点 recharts 官方文档也有提示。
jsdom 单元测试变种
jsdom 里 getBoundingClientRect 全返回 0,ResizeObserver 也量不到尺寸,导致 ResponsiveContainer 永远不克隆子组件、SVG 不出现。用 vi.mock 绕过它:
vi.mock("recharts", async (importOriginal) => {
const actual = await importOriginal<typeof import("recharts")>();
const { cloneElement } = await import("react");
return {
...actual,
ResponsiveContainer: ({ children }: { children: any }) =>
cloneElement(children, { width: 600, height: 300 }),
};
});这模拟了 ResponsiveContainer 的真实行为(它本来就是把量到的宽高 clone 进子组件),只是把"量"改成了写死数字。之后 container.querySelector("svg") 就能拿到节点了。
注意:视觉正确性(图形是否真的画出来、dark/light token 对不对)还是要靠截图验证,DOM 断言只能确认 SVG 存在,无法确认里面有没有路径被裁掉。
一句话外卖
ResponsiveContainer量父元素——把它扔进 flex 收缩容器时,w-full解析为 0,用w-[显式值] max-w-full代替。