@hsu-react/ui 2.5.4 → 2.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/es/components/ChainGraph/_hooks/useChainGraphData.js +29 -8
  2. package/es/components/ChainGraph/_hooks/useChainGraphLayout.js +14 -3
  3. package/es/components/Chart/Bar/index.js +55 -20
  4. package/es/components/Chart/Bubble/index.js +21 -6
  5. package/es/components/Chart/Common/index.js +6 -1
  6. package/es/components/Chart/Gauge/index.js +6 -1
  7. package/es/components/Chart/Heatmap/index.js +24 -7
  8. package/es/components/Chart/Line/index.js +55 -20
  9. package/es/components/Chart/Pie/Pie3D/index.js +30 -12
  10. package/es/components/Chart/Pie/index.js +24 -7
  11. package/es/components/Chart/Polar/index.js +9 -2
  12. package/es/components/Chart/Radar/index.js +6 -1
  13. package/es/components/Chart/Sankey/index.js +11 -2
  14. package/es/components/Chart/Tree/index.js +11 -2
  15. package/es/components/Select/_hooks/useSelectComposition.js +6 -2
  16. package/es/components/Table/_hooks/useAutoScrolling.js +16 -7
  17. package/es/hooks/useShallowStable.d.ts +32 -0
  18. package/es/hooks/useShallowStable.js +54 -0
  19. package/es/layout/Menu/_hooks/useOnlyLvOneMenu.js +9 -2
  20. package/lib/components/ChainGraph/_hooks/useChainGraphData.js +24 -7
  21. package/lib/components/ChainGraph/_hooks/useChainGraphLayout.js +12 -2
  22. package/lib/components/Chart/Bar/index.js +51 -19
  23. package/lib/components/Chart/Bubble/index.js +20 -6
  24. package/lib/components/Chart/Common/index.js +6 -1
  25. package/lib/components/Chart/Gauge/index.js +6 -1
  26. package/lib/components/Chart/Heatmap/index.js +23 -7
  27. package/lib/components/Chart/Line/index.js +51 -19
  28. package/lib/components/Chart/Pie/Pie3D/index.js +29 -12
  29. package/lib/components/Chart/Pie/index.js +23 -7
  30. package/lib/components/Chart/Polar/index.js +9 -2
  31. package/lib/components/Chart/Radar/index.js +6 -1
  32. package/lib/components/Chart/Sankey/index.js +10 -2
  33. package/lib/components/Chart/Tree/index.js +10 -2
  34. package/lib/components/Select/_hooks/useSelectComposition.js +6 -2
  35. package/lib/components/Table/_hooks/useAutoScrolling.js +16 -7
  36. package/lib/hooks/useShallowStable.d.ts +32 -0
  37. package/lib/hooks/useShallowStable.js +59 -0
  38. package/lib/layout/Menu/_hooks/useOnlyLvOneMenu.js +8 -2
  39. package/package.json +1 -1
  40. package/src/__tests__/callbackDepsGuard.test.ts +72 -0
  41. package/src/__tests__/depsScan.ts +158 -0
  42. package/src/__tests__/freshObjectDepsGuard.test.ts +144 -0
  43. package/src/components/ChainGraph/_hooks/callbacks.test.tsx +155 -0
  44. package/src/components/ChainGraph/_hooks/useChainGraphData.ts +38 -10
  45. package/src/components/ChainGraph/_hooks/useChainGraphLayout.ts +16 -3
  46. package/src/components/Chart/Bar/index.tsx +65 -22
  47. package/src/components/Chart/Bubble/index.tsx +21 -6
  48. package/src/components/Chart/Common/index.tsx +6 -1
  49. package/src/components/Chart/Gauge/index.tsx +6 -1
  50. package/src/components/Chart/Heatmap/index.tsx +39 -17
  51. package/src/components/Chart/Line/index.tsx +65 -22
  52. package/src/components/Chart/Pie/Pie3D/index.tsx +27 -8
  53. package/src/components/Chart/Pie/index.tsx +25 -7
  54. package/src/components/Chart/Polar/index.tsx +10 -2
  55. package/src/components/Chart/Radar/index.tsx +6 -1
  56. package/src/components/Chart/Sankey/index.tsx +10 -4
  57. package/src/components/Chart/Tree/index.tsx +10 -4
  58. package/src/components/Chart/callbacks.test.tsx +315 -0
  59. package/src/components/Chart/optionMemo.test.tsx +232 -0
  60. package/src/components/Select/_hooks/useSelectComposition.test.ts +47 -0
  61. package/src/components/Select/_hooks/useSelectComposition.ts +6 -2
  62. package/src/components/Table/_hooks/useAutoScrolling.test.tsx +95 -0
  63. package/src/components/Table/_hooks/useAutoScrolling.ts +16 -7
  64. package/src/hooks/useShallowStable.ts +65 -0
  65. package/src/layout/Menu/_hooks/useOnlyLvOneMenu.test.tsx +56 -0
  66. package/src/layout/Menu/_hooks/useOnlyLvOneMenu.ts +8 -2
@@ -13,6 +13,8 @@ var _autoSmooth = require("../_utils/autoSmooth");
13
13
  var _cartesian = require("../_utils/cartesian");
14
14
  var _chartTheme = require("../_utils/chartTheme");
15
15
  var _useIsDark = _interopRequireDefault(require("../../../hooks/useIsDark"));
16
+ var _useLatestRef = _interopRequireDefault(require("../../../hooks/useLatestRef"));
17
+ var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
16
18
  var _jsxRuntime = require("react/jsx-runtime");
17
19
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
20
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -42,8 +44,12 @@ const ChartBar = props => {
42
44
  enableLegendAutoScroll = false,
43
45
  legendVisibleCount = 8,
44
46
  legendScrollInterval = 1500,
45
- ...coreOption
47
+ ...restOption
46
48
  } = props;
49
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
50
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
51
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
52
+ const coreOption = (0, _useShallowStable.default)(restOption);
47
53
  const chartRef = (0, _react.useRef)(null);
48
54
  const chartInstanceRef = (0, _react.useRef)(null);
49
55
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -57,6 +63,29 @@ const ChartBar = props => {
57
63
  const isDark = (0, _useIsDark.default)();
58
64
  const chrome = (0, _react.useMemo)(() => (0, _chartTheme.resolveChartChrome)(isDark), [isDark]);
59
65
 
66
+ // 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
67
+ // 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
68
+ // 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
69
+ // 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
70
+ // 这样「从无到有传入回调」仍会重新注册,不会漏。
71
+ const onClickRef = (0, _useLatestRef.default)(onClick);
72
+ const onLegendSelectChangedRef = (0, _useLatestRef.default)(onLegendSelectChanged);
73
+ const onDataZoomWindowChangedRef = (0, _useLatestRef.default)(onDataZoomWindowChanged);
74
+ const hasOnClick = !!onClick;
75
+ const hasOnLegendSelectChanged = !!onLegendSelectChanged;
76
+ const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
77
+ // 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
78
+ // 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
79
+ const notifiedZoomWindowRef = (0, _react.useRef)(null);
80
+ const emitZoomWindow = (0, _react.useCallback)(next => {
81
+ const prev = notifiedZoomWindowRef.current;
82
+ if (prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex) {
83
+ return;
84
+ }
85
+ notifiedZoomWindowRef.current = next;
86
+ onDataZoomWindowChangedRef.current?.(next);
87
+ }, [onDataZoomWindowChangedRef]);
88
+
60
89
  // Cache the chart option with useMemo
61
90
  const chartOption = (0, _react.useMemo)(() => {
62
91
  const def_xAxis = (0, _cartesian.createDefaultCategoryXAxis)(xAxisData, chrome);
@@ -241,15 +270,15 @@ const ChartBar = props => {
241
270
  }
242
271
 
243
272
  // Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
244
- if (onDataZoomWindowChanged) {
273
+ if (hasOnDataZoomWindowChanged) {
245
274
  const zooms = chart.getOption()?.dataZoom;
246
275
  const zoom = zooms?.[0];
247
276
  const totalLen = xAxisData?.length ?? 0;
248
277
  if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
249
- onDataZoomWindowChanged((0, _cartesian.percentWindowToIndexWindow)(zoom.start, zoom.end, totalLen));
278
+ emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(zoom.start, zoom.end, totalLen));
250
279
  } else {
251
280
  // Without dataZoom (e.g. scrolling not enabled due to insufficient data) the window is the full range; sync once to avoid a stale old window
252
- onDataZoomWindowChanged({
281
+ emitZoomWindow({
253
282
  startIndex: 0,
254
283
  endIndex: Math.max(0, totalLen - 1)
255
284
  });
@@ -265,16 +294,19 @@ const ChartBar = props => {
265
294
  resizeObserverRef.current.observe(chartRef.current);
266
295
  }
267
296
 
268
- // Add click event
269
- if (onClick) {
270
- chartInstanceRef.current?.on("click", onClick);
297
+ // Add click event(注册与否看布尔量,实际调用取最新引用)
298
+ const handleClick = event => {
299
+ onClickRef.current?.(event);
300
+ };
301
+ if (hasOnClick) {
302
+ chartInstanceRef.current?.on("click", handleClick);
271
303
  }
272
304
 
273
305
  // Legend selection event
274
306
  const handleLegendSelectChanged = params => {
275
- onLegendSelectChanged?.(params.selected);
307
+ onLegendSelectChangedRef.current?.(params.selected);
276
308
  };
277
- if (onLegendSelectChanged) {
309
+ if (hasOnLegendSelectChanged) {
278
310
  chartInstanceRef.current?.on("legendselectchanged", handleLegendSelectChanged);
279
311
  }
280
312
 
@@ -283,15 +315,15 @@ const ChartBar = props => {
283
315
  const raw = params;
284
316
  const info = raw?.batch?.[0] ?? raw;
285
317
  if (typeof info?.startValue === "number" && typeof info?.endValue === "number") {
286
- onDataZoomWindowChanged?.({
318
+ emitZoomWindow({
287
319
  startIndex: Math.max(0, Math.round(info.startValue)),
288
320
  endIndex: Math.max(0, Math.round(info.endValue))
289
321
  });
290
322
  } else if (typeof info?.start === "number" && typeof info?.end === "number") {
291
- onDataZoomWindowChanged?.((0, _cartesian.percentWindowToIndexWindow)(info.start, info.end, xAxisData?.length ?? 0));
323
+ emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(info.start, info.end, xAxisData?.length ?? 0));
292
324
  }
293
325
  };
294
- if (onDataZoomWindowChanged) {
326
+ if (hasOnDataZoomWindowChanged) {
295
327
  chartInstanceRef.current?.on("datazoom", handleDataZoom);
296
328
  }
297
329
 
@@ -318,13 +350,13 @@ const ChartBar = props => {
318
350
  // Cleanup function
319
351
  return () => {
320
352
  window.removeEventListener("resize", handleResize);
321
- if (onClick) {
322
- chartInstanceRef.current?.off("click", onClick);
353
+ if (hasOnClick) {
354
+ chartInstanceRef.current?.off("click", handleClick);
323
355
  }
324
- if (onLegendSelectChanged) {
356
+ if (hasOnLegendSelectChanged) {
325
357
  chartInstanceRef.current?.off("legendselectchanged", handleLegendSelectChanged);
326
358
  }
327
- if (onDataZoomWindowChanged) {
359
+ if (hasOnDataZoomWindowChanged) {
328
360
  chartInstanceRef.current?.off("datazoom", handleDataZoom);
329
361
  }
330
362
  if (legendScrollRef.current) {
@@ -332,7 +364,7 @@ const ChartBar = props => {
332
364
  legendScrollRef.current = null;
333
365
  }
334
366
  };
335
- }, [chartOption, handleResize, onClick, onLegendSelectChanged, onDataZoomWindowChanged, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
367
+ }, [chartOption, handleResize, onClickRef, onLegendSelectChangedRef, hasOnClick, hasOnLegendSelectChanged, hasOnDataZoomWindowChanged, emitZoomWindow, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
336
368
  // 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
337
369
  normalizedScroll, containerReady]);
338
370
 
@@ -351,10 +383,10 @@ const ChartBar = props => {
351
383
  startIndex: normalizedScroll.startIndex,
352
384
  autoPlay: normalizedScroll.autoScroll,
353
385
  enableWheelScroll,
354
- onWindowChange: onDataZoomWindowChanged ? (startPercent, endPercent) => onDataZoomWindowChanged((0, _cartesian.percentWindowToIndexWindow)(startPercent, endPercent, xAxisData.length)) : undefined
386
+ onWindowChange: hasOnDataZoomWindowChanged ? (startPercent, endPercent) => emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(startPercent, endPercent, xAxisData.length)) : undefined
355
387
  });
356
388
  return dispose;
357
- }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, onDataZoomWindowChanged]);
389
+ }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, hasOnDataZoomWindowChanged, emitZoomWindow]);
358
390
 
359
391
  // Clean up resources when the component unmounts
360
392
  (0, _react.useEffect)(() => {
@@ -9,6 +9,7 @@ var _indexModule = _interopRequireDefault(require("../index.module.scss"));
9
9
  var echarts = _interopRequireWildcard(require("echarts"));
10
10
  var _bubble = require("../_utils/bubble");
11
11
  var _useLatestRef = require("../../../hooks/useLatestRef");
12
+ var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
12
13
  var _jsxRuntime = require("react/jsx-runtime");
13
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
15
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -28,8 +29,12 @@ const Bubble = props => {
28
29
  title,
29
30
  onChart,
30
31
  onClick,
31
- ...coreOption
32
+ ...restOption
32
33
  } = props;
34
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
35
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
36
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
37
+ const coreOption = (0, _useShallowStable.default)(restOption);
33
38
  const chartRef = (0, _react.useRef)(null);
34
39
  const chartInstanceRef = (0, _react.useRef)(null);
35
40
  const resizeObserverRef = (0, _react.useRef)(null);
@@ -257,6 +262,12 @@ const Bubble = props => {
257
262
  }, [chartOption, data]);
258
263
  const onChartRef = (0, _useLatestRef.useLatestRef)(onChart);
259
264
 
265
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
266
+ // 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
267
+ // 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
268
+ const onClickRef = (0, _useLatestRef.useLatestRef)(onClick);
269
+ const hasOnClick = !!onClick;
270
+
260
271
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
261
272
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
262
273
  // Initialize the chart
@@ -282,18 +293,21 @@ const Bubble = props => {
282
293
  window.addEventListener("resize", handleResize);
283
294
 
284
295
  // Add click event
285
- if (onClick) {
286
- chartInstanceRef.current?.on("click", onClick);
296
+ const handleClick = event => {
297
+ onClickRef.current?.(event);
298
+ };
299
+ if (hasOnClick) {
300
+ chartInstanceRef.current?.on("click", handleClick);
287
301
  }
288
302
 
289
303
  // Cleanup function
290
304
  return () => {
291
305
  window.removeEventListener("resize", handleResize);
292
- if (onClick) {
293
- chartInstanceRef.current?.off("click", onClick);
306
+ if (hasOnClick) {
307
+ chartInstanceRef.current?.off("click", handleClick);
294
308
  }
295
309
  };
296
- }, [chartOption, containerSize.width, containerSize.height, handleResize, onChartRef, onClick]);
310
+ }, [chartOption, containerSize.width, containerSize.height, handleResize, onChartRef, onClickRef, hasOnClick]);
297
311
 
298
312
  // Dispose the chart instance when the component unmounts
299
313
  (0, _react.useEffect)(() => {
@@ -9,6 +9,7 @@ var _useContainerReady = _interopRequireDefault(require("../_hooks/useContainerR
9
9
  var _indexModule = _interopRequireDefault(require("../index.module.scss"));
10
10
  var echarts = _interopRequireWildcard(require("echarts"));
11
11
  var _useLatestRef = require("../../../hooks/useLatestRef");
12
+ var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
12
13
  var _jsxRuntime = require("react/jsx-runtime");
13
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
15
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -18,8 +19,12 @@ const Common = props => {
18
19
  className,
19
20
  style,
20
21
  onChart,
21
- ...coreOption
22
+ ...restOption
22
23
  } = props;
24
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
25
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
26
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
27
+ const coreOption = (0, _useShallowStable.default)(restOption);
23
28
  const chartRef = (0, _react.useRef)(null);
24
29
  const chartInstanceRef = (0, _react.useRef)(null);
25
30
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -8,6 +8,7 @@ var echarts = _interopRequireWildcard(require("echarts"));
8
8
  var _useContainerReady = _interopRequireDefault(require("../_hooks/useContainerReady"));
9
9
  var _react = _interopRequireWildcard(require("react"));
10
10
  var _indexModule = _interopRequireDefault(require("../index.module.scss"));
11
+ var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
11
12
  var _jsxRuntime = require("react/jsx-runtime");
12
13
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
14
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -29,8 +30,12 @@ const Gauge = props => {
29
30
  axisLabel,
30
31
  title,
31
32
  color = "#1675FB",
32
- ...coreSeries
33
+ ...restSeries
33
34
  } = series;
35
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
36
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
37
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
38
+ const coreSeries = (0, _useShallowStable.default)(restSeries);
34
39
  const chartRef = (0, _react.useRef)(null);
35
40
  const chartInstanceRef = (0, _react.useRef)(null);
36
41
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -12,10 +12,13 @@ var _heatmap = require("../_utils/heatmap");
12
12
  var _chartTheme = require("../_utils/chartTheme");
13
13
  var _useIsDark = _interopRequireDefault(require("../../../hooks/useIsDark"));
14
14
  var _useLatestRef = require("../../../hooks/useLatestRef");
15
+ var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
15
16
  var _jsxRuntime = require("react/jsx-runtime");
16
17
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
18
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
18
19
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
20
+ // 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
21
+ const DEFAULT_IN_RANGE_COLOR = ["#8BCCFB", "#75C3FB", "#5AB5F6", "#42AEFA", "#2DA1F5", "#0F89E1", "#056FBB", "#025794", "#0A4089"];
19
22
  const Heatmap = props => {
20
23
  const {
21
24
  className,
@@ -34,9 +37,13 @@ const Heatmap = props => {
34
37
  onChart,
35
38
  onClick,
36
39
  zeroColor,
37
- inRangeColor = ["#8BCCFB", "#75C3FB", "#5AB5F6", "#42AEFA", "#2DA1F5", "#0F89E1", "#056FBB", "#025794", "#0A4089"],
38
- ...coreOption
40
+ inRangeColor = DEFAULT_IN_RANGE_COLOR,
41
+ ...restOption
39
42
  } = props;
43
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
44
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
45
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
46
+ const coreOption = (0, _useShallowStable.default)(restOption);
40
47
  const chartRef = (0, _react.useRef)(null);
41
48
  const chartInstanceRef = (0, _react.useRef)(null);
42
49
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -194,6 +201,12 @@ const Heatmap = props => {
194
201
  }, []);
195
202
  const onChartRef = (0, _useLatestRef.useLatestRef)(onChart);
196
203
 
204
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
205
+ // 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
206
+ // 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
207
+ const onClickRef = (0, _useLatestRef.useLatestRef)(onClick);
208
+ const hasOnClick = !!onClick;
209
+
197
210
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
198
211
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
199
212
  // Initialize the chart
@@ -222,18 +235,21 @@ const Heatmap = props => {
222
235
  }
223
236
 
224
237
  // Add click event
225
- if (onClick) {
226
- chartInstanceRef.current?.on("click", onClick);
238
+ const handleClick = event => {
239
+ onClickRef.current?.(event);
240
+ };
241
+ if (hasOnClick) {
242
+ chartInstanceRef.current?.on("click", handleClick);
227
243
  }
228
244
 
229
245
  // Cleanup function
230
246
  return () => {
231
247
  window.removeEventListener("resize", handleResize);
232
- if (onClick) {
233
- chartInstanceRef.current?.off("click", onClick);
248
+ if (hasOnClick) {
249
+ chartInstanceRef.current?.off("click", handleClick);
234
250
  }
235
251
  };
236
- }, [chartOption, handleResize, onChartRef, onClick, containerReady]);
252
+ }, [chartOption, handleResize, onChartRef, onClickRef, hasOnClick, containerReady]);
237
253
 
238
254
  // Clean up resources when the component unmounts
239
255
  (0, _react.useEffect)(() => {
@@ -13,6 +13,8 @@ var _autoSmooth = require("../_utils/autoSmooth");
13
13
  var _cartesian = require("../_utils/cartesian");
14
14
  var _chartTheme = require("../_utils/chartTheme");
15
15
  var _useIsDark = _interopRequireDefault(require("../../../hooks/useIsDark"));
16
+ var _useLatestRef = _interopRequireDefault(require("../../../hooks/useLatestRef"));
17
+ var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
16
18
  var _jsxRuntime = require("react/jsx-runtime");
17
19
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
20
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -42,8 +44,12 @@ const ChartLine = props => {
42
44
  enableLegendAutoScroll = false,
43
45
  legendVisibleCount = 8,
44
46
  legendScrollInterval = 1500,
45
- ...coreOption
47
+ ...restOption
46
48
  } = props;
49
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
50
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
51
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
52
+ const coreOption = (0, _useShallowStable.default)(restOption);
47
53
  const chartRef = (0, _react.useRef)(null);
48
54
  const chartInstanceRef = (0, _react.useRef)(null);
49
55
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -57,6 +63,29 @@ const ChartLine = props => {
57
63
  const isDark = (0, _useIsDark.default)();
58
64
  const chrome = (0, _react.useMemo)(() => (0, _chartTheme.resolveChartChrome)(isDark), [isDark]);
59
65
 
66
+ // 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
67
+ // 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
68
+ // 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
69
+ // 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
70
+ // 这样「从无到有传入回调」仍会重新注册,不会漏。
71
+ const onClickRef = (0, _useLatestRef.default)(onClick);
72
+ const onLegendSelectChangedRef = (0, _useLatestRef.default)(onLegendSelectChanged);
73
+ const onDataZoomWindowChangedRef = (0, _useLatestRef.default)(onDataZoomWindowChanged);
74
+ const hasOnClick = !!onClick;
75
+ const hasOnLegendSelectChanged = !!onLegendSelectChanged;
76
+ const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
77
+ // 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
78
+ // 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
79
+ const notifiedZoomWindowRef = (0, _react.useRef)(null);
80
+ const emitZoomWindow = (0, _react.useCallback)(next => {
81
+ const prev = notifiedZoomWindowRef.current;
82
+ if (prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex) {
83
+ return;
84
+ }
85
+ notifiedZoomWindowRef.current = next;
86
+ onDataZoomWindowChangedRef.current?.(next);
87
+ }, [onDataZoomWindowChangedRef]);
88
+
60
89
  // Cache the chart option with useMemo
61
90
  const chartOption = (0, _react.useMemo)(() => {
62
91
  const def_xAxis = (0, _cartesian.createDefaultCategoryXAxis)(xAxisData, chrome);
@@ -238,15 +267,15 @@ const ChartLine = props => {
238
267
  }
239
268
 
240
269
  // Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
241
- if (onDataZoomWindowChanged) {
270
+ if (hasOnDataZoomWindowChanged) {
242
271
  const zooms = chart.getOption()?.dataZoom;
243
272
  const zoom = zooms?.[0];
244
273
  const totalLen = xAxisData?.length ?? 0;
245
274
  if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
246
- onDataZoomWindowChanged((0, _cartesian.percentWindowToIndexWindow)(zoom.start, zoom.end, totalLen));
275
+ emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(zoom.start, zoom.end, totalLen));
247
276
  } else {
248
277
  // Without dataZoom (e.g. scrolling not enabled due to insufficient data) the window is the full range; sync once to avoid a stale old window
249
- onDataZoomWindowChanged({
278
+ emitZoomWindow({
250
279
  startIndex: 0,
251
280
  endIndex: Math.max(0, totalLen - 1)
252
281
  });
@@ -262,16 +291,19 @@ const ChartLine = props => {
262
291
  resizeObserverRef.current.observe(chartRef.current);
263
292
  }
264
293
 
265
- // Add click event
266
- if (onClick) {
267
- chartInstanceRef.current?.on("click", onClick);
294
+ // Add click event(注册与否看布尔量,实际调用取最新引用)
295
+ const handleClick = event => {
296
+ onClickRef.current?.(event);
297
+ };
298
+ if (hasOnClick) {
299
+ chartInstanceRef.current?.on("click", handleClick);
268
300
  }
269
301
 
270
302
  // Legend selection event
271
303
  const handleLegendSelectChanged = params => {
272
- onLegendSelectChanged?.(params.selected);
304
+ onLegendSelectChangedRef.current?.(params.selected);
273
305
  };
274
- if (onLegendSelectChanged) {
306
+ if (hasOnLegendSelectChanged) {
275
307
  chartInstanceRef.current?.on("legendselectchanged", handleLegendSelectChanged);
276
308
  }
277
309
 
@@ -280,15 +312,15 @@ const ChartLine = props => {
280
312
  const raw = params;
281
313
  const info = raw?.batch?.[0] ?? raw;
282
314
  if (typeof info?.startValue === "number" && typeof info?.endValue === "number") {
283
- onDataZoomWindowChanged?.({
315
+ emitZoomWindow({
284
316
  startIndex: Math.max(0, Math.round(info.startValue)),
285
317
  endIndex: Math.max(0, Math.round(info.endValue))
286
318
  });
287
319
  } else if (typeof info?.start === "number" && typeof info?.end === "number") {
288
- onDataZoomWindowChanged?.((0, _cartesian.percentWindowToIndexWindow)(info.start, info.end, xAxisData?.length ?? 0));
320
+ emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(info.start, info.end, xAxisData?.length ?? 0));
289
321
  }
290
322
  };
291
- if (onDataZoomWindowChanged) {
323
+ if (hasOnDataZoomWindowChanged) {
292
324
  chartInstanceRef.current?.on("datazoom", handleDataZoom);
293
325
  }
294
326
 
@@ -315,13 +347,13 @@ const ChartLine = props => {
315
347
  // Cleanup function
316
348
  return () => {
317
349
  window.removeEventListener("resize", handleResize);
318
- if (onClick) {
319
- chartInstanceRef.current?.off("click", onClick);
350
+ if (hasOnClick) {
351
+ chartInstanceRef.current?.off("click", handleClick);
320
352
  }
321
- if (onLegendSelectChanged) {
353
+ if (hasOnLegendSelectChanged) {
322
354
  chartInstanceRef.current?.off("legendselectchanged", handleLegendSelectChanged);
323
355
  }
324
- if (onDataZoomWindowChanged) {
356
+ if (hasOnDataZoomWindowChanged) {
325
357
  chartInstanceRef.current?.off("datazoom", handleDataZoom);
326
358
  }
327
359
  if (legendScrollRef.current) {
@@ -329,7 +361,7 @@ const ChartLine = props => {
329
361
  legendScrollRef.current = null;
330
362
  }
331
363
  };
332
- }, [chartOption, handleResize, onClick, onLegendSelectChanged, onDataZoomWindowChanged, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
364
+ }, [chartOption, handleResize, onClickRef, onLegendSelectChangedRef, hasOnClick, hasOnLegendSelectChanged, hasOnDataZoomWindowChanged, emitZoomWindow, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
333
365
  // 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
334
366
  normalizedScroll, containerReady]);
335
367
 
@@ -348,10 +380,10 @@ const ChartLine = props => {
348
380
  startIndex: normalizedScroll.startIndex,
349
381
  autoPlay: normalizedScroll.autoScroll,
350
382
  enableWheelScroll,
351
- onWindowChange: onDataZoomWindowChanged ? (startPercent, endPercent) => onDataZoomWindowChanged((0, _cartesian.percentWindowToIndexWindow)(startPercent, endPercent, xAxisData.length)) : undefined
383
+ onWindowChange: hasOnDataZoomWindowChanged ? (startPercent, endPercent) => emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(startPercent, endPercent, xAxisData.length)) : undefined
352
384
  });
353
385
  return dispose;
354
- }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, onDataZoomWindowChanged]);
386
+ }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, hasOnDataZoomWindowChanged, emitZoomWindow]);
355
387
 
356
388
  // Clean up resources when the component unmounts
357
389
  (0, _react.useEffect)(() => {
@@ -12,10 +12,18 @@ var _hooks = require("./_hooks");
12
12
  var _option = require("./_utils/option");
13
13
  var _indexModule = _interopRequireDefault(require("../../index.module.scss"));
14
14
  var _useLatestRef = require("../../../../hooks/useLatestRef");
15
+ var _useShallowStable = _interopRequireDefault(require("../../../../hooks/useShallowStable"));
15
16
  var _jsxRuntime = require("react/jsx-runtime");
16
17
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
18
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
18
19
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
20
+ // 解构默认值写成字面量会每次渲染新建一个对象,进依赖数组就让 memo 恒不命中;提到模块级常量
21
+ const DEFAULT_TOOLTIP = {
22
+ show: false
23
+ };
24
+ const DEFAULT_LABEL = {
25
+ show: true
26
+ };
19
27
  const ChartPie3D = props => {
20
28
  const {
21
29
  className,
@@ -30,17 +38,17 @@ const ChartPie3D = props => {
30
38
  minHeight = 1,
31
39
  maxHeight = 10,
32
40
  yOffset = -0.2,
33
- tooltip = {
34
- show: false
35
- },
36
- label = {
37
- show: true
38
- },
41
+ tooltip = DEFAULT_TOOLTIP,
42
+ label = DEFAULT_LABEL,
39
43
  enableMouseControl = false,
40
44
  onChart,
41
45
  onClick,
42
- ...coreOption
46
+ ...restOption
43
47
  } = props;
48
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
49
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
50
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
51
+ const coreOption = (0, _useShallowStable.default)(restOption);
44
52
  const chartRef = (0, _react.useRef)(null);
45
53
  const chartInstanceRef = (0, _react.useRef)(null);
46
54
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -87,6 +95,12 @@ const ChartPie3D = props => {
87
95
  });
88
96
  const onChartRef = (0, _useLatestRef.useLatestRef)(onChart);
89
97
 
98
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑会重建
99
+ // 3D 饼的 option 并重播动画。注册与否看布尔量(保证「从无到有传入」仍会注册),
100
+ // 实际调用取 ref 里的最新引用。
101
+ const onClickRef = (0, _useLatestRef.useLatestRef)(onClick);
102
+ const hasOnClick = !!onClick;
103
+
90
104
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
91
105
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
92
106
  (0, _react.useEffect)(() => {
@@ -106,20 +120,23 @@ const ChartPie3D = props => {
106
120
  }
107
121
  chart.on("mouseover", handleMouseOver);
108
122
  chart.on("globalout", handleGlobalOut);
109
- if (onClick) {
110
- chart.on("click", onClick);
123
+ const handleClick = event => {
124
+ onClickRef.current?.(event);
125
+ };
126
+ if (hasOnClick) {
127
+ chart.on("click", handleClick);
111
128
  }
112
129
  return () => {
113
130
  window.removeEventListener("resize", handleResize);
114
131
  if (chart) {
115
132
  chart.off("mouseover", handleMouseOver);
116
133
  chart.off("globalout", handleGlobalOut);
117
- if (onClick) {
118
- chart.off("click", onClick);
134
+ if (hasOnClick) {
135
+ chart.off("click", handleClick);
119
136
  }
120
137
  }
121
138
  };
122
- }, [chartOption, handleResize, handleMouseOver, handleGlobalOut, onChartRef, onClick, containerReady]);
139
+ }, [chartOption, handleResize, handleMouseOver, handleGlobalOut, onChartRef, onClickRef, hasOnClick, containerReady]);
123
140
  (0, _react.useEffect)(() => {
124
141
  return () => {
125
142
  if (resizeObserverRef.current) {