@hsu-react/ui 2.5.3 → 2.5.5

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 (90) 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 +49 -19
  4. package/es/components/Chart/Bubble/index.js +21 -6
  5. package/es/components/Chart/Common/index.js +7 -2
  6. package/es/components/Chart/Heatmap/index.js +21 -6
  7. package/es/components/Chart/Line/index.js +49 -19
  8. package/es/components/Chart/Pie/Pie3D/index.js +22 -6
  9. package/es/components/Chart/Pie/index.js +21 -6
  10. package/es/components/Chart/Sankey/index.js +11 -2
  11. package/es/components/Chart/Tree/index.js +11 -2
  12. package/es/components/Chat/ChatInput/index.js +7 -2
  13. package/es/components/Icon/index.js +8 -2
  14. package/es/components/Input/Number/index.js +28 -21
  15. package/es/components/Input/Password/index.js +44 -36
  16. package/es/components/Input/Search/index.js +44 -36
  17. package/es/components/Input/TextArea/index.js +64 -42
  18. package/es/components/Input/index.js +40 -34
  19. package/es/components/Select/_hooks/useSelectComposition.js +6 -2
  20. package/es/components/Select/_hooks/useSelectInputOptions.js +8 -2
  21. package/es/components/Slider/index.js +24 -15
  22. package/es/components/Table/_hooks/useAutoScrolling.js +16 -7
  23. package/es/components/Upload/_hooks/useUploadFileList.js +8 -2
  24. package/es/components/Upload/_hooks/useUploadOperations.js +8 -2
  25. package/es/hooks/useLatestRef.d.ts +24 -0
  26. package/es/hooks/useLatestRef.js +29 -0
  27. package/es/layout/Menu/_hooks/useOnlyLvOneMenu.js +9 -2
  28. package/lib/components/ChainGraph/_hooks/useChainGraphData.js +24 -7
  29. package/lib/components/ChainGraph/_hooks/useChainGraphLayout.js +12 -2
  30. package/lib/components/Chart/Bar/index.js +45 -18
  31. package/lib/components/Chart/Bubble/index.js +19 -6
  32. package/lib/components/Chart/Common/index.js +6 -2
  33. package/lib/components/Chart/Heatmap/index.js +19 -6
  34. package/lib/components/Chart/Line/index.js +45 -18
  35. package/lib/components/Chart/Pie/Pie3D/index.js +20 -6
  36. package/lib/components/Chart/Pie/index.js +19 -6
  37. package/lib/components/Chart/Sankey/index.js +10 -2
  38. package/lib/components/Chart/Tree/index.js +10 -2
  39. package/lib/components/Chat/ChatInput/index.js +6 -2
  40. package/lib/components/Icon/index.js +7 -2
  41. package/lib/components/Input/Number/index.js +27 -18
  42. package/lib/components/Input/Password/index.js +40 -26
  43. package/lib/components/Input/Search/index.js +42 -26
  44. package/lib/components/Input/TextArea/index.js +54 -26
  45. package/lib/components/Input/index.js +37 -24
  46. package/lib/components/Select/_hooks/useSelectComposition.js +6 -2
  47. package/lib/components/Select/_hooks/useSelectInputOptions.js +7 -2
  48. package/lib/components/Slider/index.js +23 -11
  49. package/lib/components/Table/_hooks/useAutoScrolling.js +16 -7
  50. package/lib/components/Upload/_hooks/useUploadFileList.js +7 -2
  51. package/lib/components/Upload/_hooks/useUploadOperations.js +7 -2
  52. package/lib/hooks/useLatestRef.d.ts +24 -0
  53. package/lib/hooks/useLatestRef.js +35 -0
  54. package/lib/layout/Menu/_hooks/useOnlyLvOneMenu.js +8 -2
  55. package/package.json +9 -3
  56. package/src/__tests__/callbackDepsGuard.test.ts +119 -0
  57. package/src/__tests__/setup.ts +26 -0
  58. package/src/components/ChainGraph/_hooks/callbacks.test.tsx +155 -0
  59. package/src/components/ChainGraph/_hooks/useChainGraphData.ts +38 -10
  60. package/src/components/ChainGraph/_hooks/useChainGraphLayout.ts +16 -3
  61. package/src/components/Chart/Bar/index.tsx +59 -21
  62. package/src/components/Chart/Bubble/index.tsx +22 -7
  63. package/src/components/Chart/Common/index.tsx +7 -2
  64. package/src/components/Chart/Heatmap/index.tsx +25 -6
  65. package/src/components/Chart/Line/index.tsx +59 -21
  66. package/src/components/Chart/Pie/Pie3D/index.tsx +22 -7
  67. package/src/components/Chart/Pie/index.tsx +22 -7
  68. package/src/components/Chart/Sankey/index.tsx +10 -4
  69. package/src/components/Chart/Tree/index.tsx +10 -4
  70. package/src/components/Chart/callbacks.test.tsx +315 -0
  71. package/src/components/Chat/ChatInput/index.tsx +7 -2
  72. package/src/components/Icon/index.tsx +7 -2
  73. package/src/components/Input/Number/index.tsx +25 -23
  74. package/src/components/Input/Password/index.tsx +37 -26
  75. package/src/components/Input/Search/index.tsx +39 -26
  76. package/src/components/Input/TextArea/index.test.tsx +208 -0
  77. package/src/components/Input/TextArea/index.tsx +56 -30
  78. package/src/components/Input/index.test.tsx +189 -0
  79. package/src/components/Input/index.tsx +36 -29
  80. package/src/components/Select/_hooks/useSelectComposition.test.ts +47 -0
  81. package/src/components/Select/_hooks/useSelectComposition.ts +6 -2
  82. package/src/components/Select/_hooks/useSelectInputOptions.ts +6 -9
  83. package/src/components/Slider/index.tsx +22 -12
  84. package/src/components/Table/_hooks/useAutoScrolling.test.tsx +95 -0
  85. package/src/components/Table/_hooks/useAutoScrolling.ts +16 -7
  86. package/src/components/Upload/_hooks/useUploadFileList.ts +6 -2
  87. package/src/components/Upload/_hooks/useUploadOperations.ts +7 -2
  88. package/src/hooks/useLatestRef.ts +31 -0
  89. package/src/layout/Menu/_hooks/useOnlyLvOneMenu.test.tsx +56 -0
  90. package/src/layout/Menu/_hooks/useOnlyLvOneMenu.ts +8 -2
@@ -24,6 +24,7 @@ import {
24
24
  } from "../_utils/cartesian";
25
25
  import { resolveChartChrome } from "../_utils/chartTheme";
26
26
  import useIsDark from "../../../hooks/useIsDark";
27
+ import useLatestRef from "../../../hooks/useLatestRef";
27
28
 
28
29
  export interface ChartLineProps extends ChartCommonProps {
29
30
  chartTitle?: string;
@@ -90,6 +91,36 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
90
91
  const isDark = useIsDark();
91
92
  const chrome = useMemo(() => resolveChartChrome(isDark), [isDark]);
92
93
 
94
+ // 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
95
+ // 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
96
+ // 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
97
+ // 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
98
+ // 这样「从无到有传入回调」仍会重新注册,不会漏。
99
+ const onClickRef = useLatestRef(onClick);
100
+ const onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
101
+ const onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
102
+ const hasOnClick = !!onClick;
103
+ const hasOnLegendSelectChanged = !!onLegendSelectChanged;
104
+ const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
105
+ // 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
106
+ // 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
107
+ const notifiedZoomWindowRef = useRef<DataZoomIndexWindow | null>(null);
108
+ const emitZoomWindow = useCallback(
109
+ (next: DataZoomIndexWindow) => {
110
+ const prev = notifiedZoomWindowRef.current;
111
+ if (
112
+ prev &&
113
+ prev.startIndex === next.startIndex &&
114
+ prev.endIndex === next.endIndex
115
+ ) {
116
+ return;
117
+ }
118
+ notifiedZoomWindowRef.current = next;
119
+ onDataZoomWindowChangedRef.current?.(next);
120
+ },
121
+ [onDataZoomWindowChangedRef],
122
+ );
123
+
93
124
  // Cache the chart option with useMemo
94
125
  const chartOption = useMemo(() => {
95
126
  const def_xAxis = createDefaultCategoryXAxis(xAxisData, chrome);
@@ -326,18 +357,18 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
326
357
  }
327
358
 
328
359
  // Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
329
- if (onDataZoomWindowChanged) {
360
+ if (hasOnDataZoomWindowChanged) {
330
361
  const zooms = (chart.getOption() as ChartsOption | undefined)
331
362
  ?.dataZoom as Array<{ start?: number; end?: number }> | undefined;
332
363
  const zoom = zooms?.[0];
333
364
  const totalLen = xAxisData?.length ?? 0;
334
365
  if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
335
- onDataZoomWindowChanged(
366
+ emitZoomWindow(
336
367
  percentWindowToIndexWindow(zoom.start, zoom.end, totalLen),
337
368
  );
338
369
  } else {
339
370
  // 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
340
- onDataZoomWindowChanged({
371
+ emitZoomWindow({
341
372
  startIndex: 0,
342
373
  endIndex: Math.max(0, totalLen - 1),
343
374
  });
@@ -353,18 +384,21 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
353
384
  resizeObserverRef.current.observe(chartRef.current);
354
385
  }
355
386
 
356
- // Add click event
357
- if (onClick) {
358
- chartInstanceRef.current?.on("click", onClick);
387
+ // Add click event(注册与否看布尔量,实际调用取最新引用)
388
+ const handleClick = (event: echarts.ECElementEvent) => {
389
+ onClickRef.current?.(event);
390
+ };
391
+ if (hasOnClick) {
392
+ chartInstanceRef.current?.on("click", handleClick);
359
393
  }
360
394
 
361
395
  // Legend selection event
362
396
  const handleLegendSelectChanged = (params: unknown) => {
363
- onLegendSelectChanged?.(
397
+ onLegendSelectChangedRef.current?.(
364
398
  (params as { selected: Record<string, boolean> }).selected,
365
399
  );
366
400
  };
367
- if (onLegendSelectChanged) {
401
+ if (hasOnLegendSelectChanged) {
368
402
  chartInstanceRef.current?.on(
369
403
  "legendselectchanged",
370
404
  handleLegendSelectChanged,
@@ -390,7 +424,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
390
424
  typeof info?.startValue === "number" &&
391
425
  typeof info?.endValue === "number"
392
426
  ) {
393
- onDataZoomWindowChanged?.({
427
+ emitZoomWindow({
394
428
  startIndex: Math.max(0, Math.round(info.startValue)),
395
429
  endIndex: Math.max(0, Math.round(info.endValue)),
396
430
  });
@@ -398,7 +432,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
398
432
  typeof info?.start === "number" &&
399
433
  typeof info?.end === "number"
400
434
  ) {
401
- onDataZoomWindowChanged?.(
435
+ emitZoomWindow(
402
436
  percentWindowToIndexWindow(
403
437
  info.start,
404
438
  info.end,
@@ -407,7 +441,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
407
441
  );
408
442
  }
409
443
  };
410
- if (onDataZoomWindowChanged) {
444
+ if (hasOnDataZoomWindowChanged) {
411
445
  chartInstanceRef.current?.on("datazoom", handleDataZoom);
412
446
  }
413
447
 
@@ -443,18 +477,18 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
443
477
  return () => {
444
478
  window.removeEventListener("resize", handleResize);
445
479
 
446
- if (onClick) {
447
- chartInstanceRef.current?.off("click", onClick);
480
+ if (hasOnClick) {
481
+ chartInstanceRef.current?.off("click", handleClick);
448
482
  }
449
483
 
450
- if (onLegendSelectChanged) {
484
+ if (hasOnLegendSelectChanged) {
451
485
  chartInstanceRef.current?.off(
452
486
  "legendselectchanged",
453
487
  handleLegendSelectChanged,
454
488
  );
455
489
  }
456
490
 
457
- if (onDataZoomWindowChanged) {
491
+ if (hasOnDataZoomWindowChanged) {
458
492
  chartInstanceRef.current?.off("datazoom", handleDataZoom);
459
493
  }
460
494
 
@@ -466,9 +500,12 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
466
500
  }, [
467
501
  chartOption,
468
502
  handleResize,
469
- onClick,
470
- onLegendSelectChanged,
471
- onDataZoomWindowChanged,
503
+ onClickRef,
504
+ onLegendSelectChangedRef,
505
+ hasOnClick,
506
+ hasOnLegendSelectChanged,
507
+ hasOnDataZoomWindowChanged,
508
+ emitZoomWindow,
472
509
  xAxisData,
473
510
  enableLegendAutoScroll,
474
511
  legendVisibleCount,
@@ -505,9 +542,9 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
505
542
  startIndex: normalizedScroll.startIndex,
506
543
  autoPlay: normalizedScroll.autoScroll,
507
544
  enableWheelScroll,
508
- onWindowChange: onDataZoomWindowChanged
545
+ onWindowChange: hasOnDataZoomWindowChanged
509
546
  ? (startPercent, endPercent) =>
510
- onDataZoomWindowChanged(
547
+ emitZoomWindow(
511
548
  percentWindowToIndexWindow(
512
549
  startPercent,
513
550
  endPercent,
@@ -523,7 +560,8 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
523
560
  normalizedScroll,
524
561
  sliderDataZoom,
525
562
  xAxisData,
526
- onDataZoomWindowChanged,
563
+ hasOnDataZoomWindowChanged,
564
+ emitZoomWindow,
527
565
  ]);
528
566
 
529
567
  // Clean up resources when the component unmounts
@@ -7,6 +7,7 @@ import { ChartCommonProps, ChartOptionType, ChartsOption } from "../..";
7
7
  import { useMouseOverHandler, useGlobalOutHandler } from "./_hooks";
8
8
  import { getPie3DOption } from "./_utils/option";
9
9
  import styles from "../../index.module.scss";
10
+ import { useLatestRef } from "../../../../hooks/useLatestRef";
10
11
 
11
12
  export interface Pie3DDataItem {
12
13
  name: string;
@@ -170,6 +171,16 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
170
171
  { minHeight, maxHeight, yOffset, autoRotate }
171
172
  );
172
173
 
174
+ const onChartRef = useLatestRef(onChart);
175
+
176
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑会重建
177
+ // 3D 饼的 option 并重播动画。注册与否看布尔量(保证「从无到有传入」仍会注册),
178
+ // 实际调用取 ref 里的最新引用。
179
+ const onClickRef = useLatestRef(onClick);
180
+ const hasOnClick = !!onClick;
181
+
182
+ // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
183
+ // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
173
184
  useEffect(() => {
174
185
  if (!chartRef.current || !containerReady) return;
175
186
 
@@ -177,7 +188,7 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
177
188
  if (!chart) {
178
189
  chart = echarts.init(chartRef.current);
179
190
  chartInstanceRef.current = chart;
180
- onChart?.(chart);
191
+ onChartRef.current?.(chart);
181
192
  }
182
193
 
183
194
  chart.setOption(chartOption as ChartOptionType, true);
@@ -192,8 +203,11 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
192
203
 
193
204
  chart.on("mouseover", handleMouseOver);
194
205
  chart.on("globalout", handleGlobalOut);
195
- if (onClick) {
196
- chart.on("click", onClick);
206
+ const handleClick = (event: echarts.ECElementEvent) => {
207
+ onClickRef.current?.(event);
208
+ };
209
+ if (hasOnClick) {
210
+ chart.on("click", handleClick);
197
211
  }
198
212
 
199
213
  return () => {
@@ -201,8 +215,8 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
201
215
  if (chart) {
202
216
  chart.off("mouseover", handleMouseOver);
203
217
  chart.off("globalout", handleGlobalOut);
204
- if (onClick) {
205
- chart.off("click", onClick);
218
+ if (hasOnClick) {
219
+ chart.off("click", handleClick);
206
220
  }
207
221
  }
208
222
  };
@@ -211,8 +225,9 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
211
225
  handleResize,
212
226
  handleMouseOver,
213
227
  handleGlobalOut,
214
- onChart,
215
- onClick,
228
+ onChartRef,
229
+ onClickRef,
230
+ hasOnClick,
216
231
  containerReady,
217
232
  ]);
218
233
 
@@ -13,6 +13,7 @@ import { autoScrollLegend } from "../chartUtils";
13
13
 
14
14
  import styles from "../index.module.scss";
15
15
  import ChartPie3D, { ChartPie3DProps } from "./Pie3D";
16
+ import { useLatestRef } from "../../../hooks/useLatestRef";
16
17
 
17
18
  export interface ChartPieProps extends ChartCommonProps {
18
19
  chartTitle?: string;
@@ -252,6 +253,16 @@ const ChartPie: ChartPieFC = (props) => {
252
253
  chartInstanceRef.current?.resize();
253
254
  }, []);
254
255
 
256
+ const onChartRef = useLatestRef(onChart);
257
+
258
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
259
+ // 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
260
+ // 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
261
+ const onClickRef = useLatestRef(onClick);
262
+ const hasOnClick = !!onClick;
263
+
264
+ // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
265
+ // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
255
266
  // Initialize the chart
256
267
  useEffect(() => {
257
268
  if (!chartRef.current || !containerReady) return;
@@ -262,7 +273,7 @@ const ChartPie: ChartPieFC = (props) => {
262
273
  chart = echarts.init(chartRef.current);
263
274
  chartInstanceRef.current = chart;
264
275
  // Call the onChart callback on first initialization
265
- onChart?.(chart);
276
+ onChartRef.current?.(chart);
266
277
  }
267
278
 
268
279
  // Apply the option
@@ -278,8 +289,11 @@ const ChartPie: ChartPieFC = (props) => {
278
289
  }
279
290
 
280
291
  // Add click event
281
- if (onClick) {
282
- chartInstanceRef.current?.on("click", onClick);
292
+ const handleClick = (event: echarts.ECElementEvent) => {
293
+ onClickRef.current?.(event);
294
+ };
295
+ if (hasOnClick) {
296
+ chartInstanceRef.current?.on("click", handleClick);
283
297
  }
284
298
 
285
299
  // Legend auto-scroll
@@ -302,8 +316,8 @@ const ChartPie: ChartPieFC = (props) => {
302
316
  return () => {
303
317
  window.removeEventListener("resize", handleResize);
304
318
 
305
- if (onClick) {
306
- chartInstanceRef.current?.off("click", onClick);
319
+ if (hasOnClick) {
320
+ chartInstanceRef.current?.off("click", handleClick);
307
321
  }
308
322
 
309
323
  // Clean up legend scrolling
@@ -315,8 +329,9 @@ const ChartPie: ChartPieFC = (props) => {
315
329
  }, [
316
330
  chartOption,
317
331
  handleResize,
318
- onChart,
319
- onClick,
332
+ onChartRef,
333
+ onClickRef,
334
+ hasOnClick,
320
335
  enableLegendAutoScroll,
321
336
  seriesData,
322
337
  legendVisibleCount,
@@ -3,6 +3,7 @@ import useContainerReady from "../_hooks/useContainerReady";
3
3
  import styles from "../index.module.scss";
4
4
  import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
5
5
  import * as echarts from "echarts";
6
+ import useLatestRef from "../../../hooks/useLatestRef";
6
7
 
7
8
  type SankeyNodeItemOption = echarts.SankeySeriesOption["data"];
8
9
  type SankeyEdgeItemOption = echarts.SankeySeriesOption["links"];
@@ -101,6 +102,13 @@ const Sankey: React.FC<ChartSankeyProps> = (props) => {
101
102
  return option;
102
103
  }, [seriesData, seriesLinks, series]);
103
104
 
105
+ // getImage 只是「出图后把 dataURL 交出去」的出口,既不决定图怎么画,
106
+ // 也不该决定要不要重画。它进依赖数组会让 effect 跟着重跑 → setOption(notMerge)
107
+ // → 图重绘 → 再触发一次 finished → 再回调;消费方把图存进 state 就是死循环。
108
+ // 而且 finished 监听只在首次 init 时注册,闭包里锁死的是首帧的 getImage,
109
+ // 后续换了引用永远调不到 —— 依赖数组写了它也没用,是条死依赖。走 ref 两个问题一起解决。
110
+ const getImageRef = useLatestRef(getImage);
111
+
104
112
  // Callback handling chart resize
105
113
  const handleResize = useCallback(() => {
106
114
  // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
@@ -123,7 +131,7 @@ const Sankey: React.FC<ChartSankeyProps> = (props) => {
123
131
 
124
132
  // Attach the finished event listener on first initialization
125
133
  chart.on("finished", () => {
126
- getImage?.(
134
+ getImageRef.current?.(
127
135
  chart!.getDataURL({
128
136
  type: "png",
129
137
  pixelRatio: 1,
@@ -149,9 +157,7 @@ const Sankey: React.FC<ChartSankeyProps> = (props) => {
149
157
  return () => {
150
158
  window.removeEventListener("resize", handleResize);
151
159
  };
152
- }, [chartOption, handleResize, getImage,
153
- containerReady,
154
- ]);
160
+ }, [chartOption, handleResize, getImageRef, containerReady]);
155
161
 
156
162
  // Clean up resources on unmount
157
163
  useEffect(() => {
@@ -8,6 +8,7 @@ import {
8
8
  SeriesData,
9
9
  } from "..";
10
10
  import * as echarts from "echarts";
11
+ import useLatestRef from "../../../hooks/useLatestRef";
11
12
  import { deepCopy } from "hsu-utils";
12
13
  import { handleTreeData } from "../_utils/tree";
13
14
 
@@ -41,6 +42,13 @@ const Tree: React.FC<ChartTreeProps> = (props) => {
41
42
  return option;
42
43
  }, [seriesData, series]);
43
44
 
45
+ // getImage 只是「出图后把 dataURL 交出去」的出口,既不决定图怎么画,
46
+ // 也不该决定要不要重画。它进依赖数组会让 effect 跟着重跑 → setOption(notMerge)
47
+ // → 图重绘 → 再触发一次 finished → 再回调;消费方把图存进 state 就是死循环。
48
+ // 而且 finished 监听只在首次 init 时注册,闭包里锁死的是首帧的 getImage,
49
+ // 后续换了引用永远调不到 —— 依赖数组写了它也没用,是条死依赖。走 ref 两个问题一起解决。
50
+ const getImageRef = useLatestRef(getImage);
51
+
44
52
  // Callback handling chart resize
45
53
  const handleResize = useCallback(() => {
46
54
  // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
@@ -63,7 +71,7 @@ const Tree: React.FC<ChartTreeProps> = (props) => {
63
71
 
64
72
  // Attach the finished event listener on first initialization
65
73
  chart.on("finished", () => {
66
- getImage?.(
74
+ getImageRef.current?.(
67
75
  chart!.getDataURL({
68
76
  type: "png",
69
77
  pixelRatio: 1,
@@ -89,9 +97,7 @@ const Tree: React.FC<ChartTreeProps> = (props) => {
89
97
  return () => {
90
98
  window.removeEventListener("resize", handleResize);
91
99
  };
92
- }, [chartOption, handleResize, getImage,
93
- containerReady,
94
- ]);
100
+ }, [chartOption, handleResize, getImageRef, containerReady]);
95
101
 
96
102
  // Clean up resources on unmount
97
103
  useEffect(() => {