@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
@@ -9,7 +9,15 @@ import { useMouseOverHandler, useGlobalOutHandler } from "./_hooks";
9
9
  import { getPie3DOption } from "./_utils/option";
10
10
  import styles from "../../index.module.scss";
11
11
  import { useLatestRef } from "../../../../hooks/useLatestRef";
12
+ import useShallowStable from "../../../../hooks/useShallowStable";
12
13
  import { jsx as _jsx } from "react/jsx-runtime";
14
+ // 解构默认值写成字面量会每次渲染新建一个对象,进依赖数组就让 memo 恒不命中;提到模块级常量
15
+ var DEFAULT_TOOLTIP = {
16
+ show: false
17
+ };
18
+ var DEFAULT_LABEL = {
19
+ show: true
20
+ };
13
21
  var ChartPie3D = function ChartPie3D(props) {
14
22
  var className = props.className,
15
23
  style = props.style,
@@ -33,18 +41,18 @@ var ChartPie3D = function ChartPie3D(props) {
33
41
  _props$yOffset = props.yOffset,
34
42
  yOffset = _props$yOffset === void 0 ? -0.2 : _props$yOffset,
35
43
  _props$tooltip = props.tooltip,
36
- tooltip = _props$tooltip === void 0 ? {
37
- show: false
38
- } : _props$tooltip,
44
+ tooltip = _props$tooltip === void 0 ? DEFAULT_TOOLTIP : _props$tooltip,
39
45
  _props$label = props.label,
40
- label = _props$label === void 0 ? {
41
- show: true
42
- } : _props$label,
46
+ label = _props$label === void 0 ? DEFAULT_LABEL : _props$label,
43
47
  _props$enableMouseCon = props.enableMouseControl,
44
48
  enableMouseControl = _props$enableMouseCon === void 0 ? false : _props$enableMouseCon,
45
49
  onChart = props.onChart,
46
50
  onClick = props.onClick,
47
- coreOption = _objectWithoutProperties(props, _excluded);
51
+ restOption = _objectWithoutProperties(props, _excluded);
52
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
53
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
54
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
55
+ var coreOption = useShallowStable(restOption);
48
56
  var chartRef = useRef(null);
49
57
  var chartInstanceRef = useRef(null);
50
58
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -92,6 +100,12 @@ var ChartPie3D = function ChartPie3D(props) {
92
100
  });
93
101
  var onChartRef = useLatestRef(onChart);
94
102
 
103
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑会重建
104
+ // 3D 饼的 option 并重播动画。注册与否看布尔量(保证「从无到有传入」仍会注册),
105
+ // 实际调用取 ref 里的最新引用。
106
+ var onClickRef = useLatestRef(onClick);
107
+ var hasOnClick = !!onClick;
108
+
95
109
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
96
110
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
97
111
  useEffect(function () {
@@ -112,20 +126,24 @@ var ChartPie3D = function ChartPie3D(props) {
112
126
  }
113
127
  chart.on("mouseover", handleMouseOver);
114
128
  chart.on("globalout", handleGlobalOut);
115
- if (onClick) {
116
- chart.on("click", onClick);
129
+ var handleClick = function handleClick(event) {
130
+ var _onClickRef$current;
131
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
132
+ };
133
+ if (hasOnClick) {
134
+ chart.on("click", handleClick);
117
135
  }
118
136
  return function () {
119
137
  window.removeEventListener("resize", handleResize);
120
138
  if (chart) {
121
139
  chart.off("mouseover", handleMouseOver);
122
140
  chart.off("globalout", handleGlobalOut);
123
- if (onClick) {
124
- chart.off("click", onClick);
141
+ if (hasOnClick) {
142
+ chart.off("click", handleClick);
125
143
  }
126
144
  }
127
145
  };
128
- }, [chartOption, handleResize, handleMouseOver, handleGlobalOut, onChartRef, onClick, containerReady]);
146
+ }, [chartOption, handleResize, handleMouseOver, handleGlobalOut, onChartRef, onClickRef, hasOnClick, containerReady]);
129
147
  useEffect(function () {
130
148
  return function () {
131
149
  if (resizeObserverRef.current) {
@@ -20,7 +20,10 @@ import { autoScrollLegend } from "../chartUtils";
20
20
  import styles from "../index.module.scss";
21
21
  import ChartPie3D from "./Pie3D";
22
22
  import { useLatestRef } from "../../../hooks/useLatestRef";
23
+ import useShallowStable from "../../../hooks/useShallowStable";
23
24
  import { jsx as _jsx } from "react/jsx-runtime";
25
+ // 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
26
+ var EMPTY_EXTEND_SERIES = [];
24
27
  var ChartPie = function ChartPie(props) {
25
28
  var className = props.className,
26
29
  style = props.style,
@@ -32,7 +35,7 @@ var ChartPie = function ChartPie(props) {
32
35
  title = props.title,
33
36
  onChart = props.onChart,
34
37
  _props$extendSeries = props.extendSeries,
35
- extendSeries = _props$extendSeries === void 0 ? [] : _props$extendSeries,
38
+ extendSeries = _props$extendSeries === void 0 ? EMPTY_EXTEND_SERIES : _props$extendSeries,
36
39
  onClick = props.onClick,
37
40
  _props$enableLegendAu = props.enableLegendAutoScroll,
38
41
  enableLegendAutoScroll = _props$enableLegendAu === void 0 ? false : _props$enableLegendAu,
@@ -48,7 +51,11 @@ var ChartPie = function ChartPie(props) {
48
51
  spanAngle = _props$spanAngle === void 0 ? 180 : _props$spanAngle,
49
52
  center = props.center,
50
53
  radius = props.radius,
51
- coreOption = _objectWithoutProperties(props, _excluded);
54
+ restOption = _objectWithoutProperties(props, _excluded);
55
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
56
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
57
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
58
+ var coreOption = useShallowStable(restOption);
52
59
  var chartRef = useRef(null);
53
60
  var chartInstanceRef = useRef(null);
54
61
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -210,6 +217,12 @@ var ChartPie = function ChartPie(props) {
210
217
  }, []);
211
218
  var onChartRef = useLatestRef(onChart);
212
219
 
220
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
221
+ // 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
222
+ // 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
223
+ var onClickRef = useLatestRef(onClick);
224
+ var hasOnClick = !!onClick;
225
+
213
226
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
214
227
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
215
228
  // Initialize the chart
@@ -239,9 +252,13 @@ var ChartPie = function ChartPie(props) {
239
252
  }
240
253
 
241
254
  // Add click event
242
- if (onClick) {
255
+ var handleClick = function handleClick(event) {
256
+ var _onClickRef$current;
257
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
258
+ };
259
+ if (hasOnClick) {
243
260
  var _chartInstanceRef$cur2;
244
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
261
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
245
262
  }
246
263
 
247
264
  // Legend auto-scroll
@@ -262,9 +279,9 @@ var ChartPie = function ChartPie(props) {
262
279
  // Cleanup function
263
280
  return function () {
264
281
  window.removeEventListener("resize", handleResize);
265
- if (onClick) {
282
+ if (hasOnClick) {
266
283
  var _chartInstanceRef$cur3;
267
- (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", onClick);
284
+ (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", handleClick);
268
285
  }
269
286
 
270
287
  // Clean up legend scrolling
@@ -273,7 +290,7 @@ var ChartPie = function ChartPie(props) {
273
290
  legendScrollRef.current = null;
274
291
  }
275
292
  };
276
- }, [chartOption, handleResize, onChartRef, onClick, enableLegendAutoScroll, seriesData, legendVisibleCount, legendScrollInterval, containerReady]);
293
+ }, [chartOption, handleResize, onChartRef, onClickRef, hasOnClick, enableLegendAutoScroll, seriesData, legendVisibleCount, legendScrollInterval, containerReady]);
277
294
 
278
295
  // Clean up resources when the component unmounts
279
296
  useEffect(function () {
@@ -11,7 +11,10 @@ import * as echarts from "echarts";
11
11
  import useContainerReady from "../_hooks/useContainerReady";
12
12
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
13
13
  import styles from "../index.module.scss";
14
+ import useShallowStable from "../../../hooks/useShallowStable";
14
15
  import { jsx as _jsx } from "react/jsx-runtime";
16
+ // 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
17
+ var DEFAULT_COLOR = ["#00FFA3", "#00FFA3"];
15
18
  var Polar = function Polar(props) {
16
19
  var className = props.className,
17
20
  style = props.style,
@@ -26,8 +29,12 @@ var Polar = function Polar(props) {
26
29
  textStyle = props.textStyle,
27
30
  seriesData = props.seriesData,
28
31
  _props$color = props.color,
29
- color = _props$color === void 0 ? ["#00FFA3", "#00FFA3"] : _props$color,
30
- coreOption = _objectWithoutProperties(props, _excluded);
32
+ color = _props$color === void 0 ? DEFAULT_COLOR : _props$color,
33
+ restOption = _objectWithoutProperties(props, _excluded);
34
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
35
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
36
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
37
+ var coreOption = useShallowStable(restOption);
31
38
  var chartRef = useRef(null);
32
39
  var chartInstanceRef = useRef(null);
33
40
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -17,6 +17,7 @@ import * as echarts from "echarts";
17
17
  import useContainerReady from "../_hooks/useContainerReady";
18
18
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
19
19
  import styles from "../index.module.scss";
20
+ import useShallowStable from "../../../hooks/useShallowStable";
20
21
 
21
22
  /** Single-indicator tooltip trigger mode
22
23
  * - `point` triggers only when hovering data points/lines (native echarts item trigger)
@@ -38,7 +39,11 @@ var Radar = function Radar(props) {
38
39
  singleIndicatorTooltipProp = _props$singleIndicato === void 0 ? false : _props$singleIndicato,
39
40
  singleIndicatorMode = props.singleIndicatorMode,
40
41
  singleIndicatorHoverFill = props.singleIndicatorHoverFill,
41
- coreOption = _objectWithoutProperties(props, _excluded);
42
+ restOption = _objectWithoutProperties(props, _excluded);
43
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
44
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
45
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
46
+ var coreOption = useShallowStable(restOption);
42
47
  var chartRef = useRef(null);
43
48
  var chartInstanceRef = useRef(null);
44
49
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -14,6 +14,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from "react";
14
14
  import useContainerReady from "../_hooks/useContainerReady";
15
15
  import styles from "../index.module.scss";
16
16
  import * as echarts from "echarts";
17
+ import useLatestRef from "../../../hooks/useLatestRef";
17
18
  import { jsx as _jsx } from "react/jsx-runtime";
18
19
  var Sankey = function Sankey(props) {
19
20
  var className = props.className,
@@ -103,6 +104,13 @@ var Sankey = function Sankey(props) {
103
104
  return option;
104
105
  }, [seriesData, seriesLinks, series]);
105
106
 
107
+ // getImage 只是「出图后把 dataURL 交出去」的出口,既不决定图怎么画,
108
+ // 也不该决定要不要重画。它进依赖数组会让 effect 跟着重跑 → setOption(notMerge)
109
+ // → 图重绘 → 再触发一次 finished → 再回调;消费方把图存进 state 就是死循环。
110
+ // 而且 finished 监听只在首次 init 时注册,闭包里锁死的是首帧的 getImage,
111
+ // 后续换了引用永远调不到 —— 依赖数组写了它也没用,是条死依赖。走 ref 两个问题一起解决。
112
+ var getImageRef = useLatestRef(getImage);
113
+
106
114
  // Callback handling chart resize
107
115
  var handleResize = useCallback(function () {
108
116
  var _chartInstanceRef$cur;
@@ -125,7 +133,8 @@ var Sankey = function Sankey(props) {
125
133
 
126
134
  // Attach the finished event listener on first initialization
127
135
  chart.on("finished", function () {
128
- getImage === null || getImage === void 0 || getImage(chart.getDataURL({
136
+ var _getImageRef$current;
137
+ (_getImageRef$current = getImageRef.current) === null || _getImageRef$current === void 0 || _getImageRef$current.call(getImageRef, chart.getDataURL({
129
138
  type: "png",
130
139
  pixelRatio: 1,
131
140
  backgroundColor: "#fff"
@@ -149,7 +158,7 @@ var Sankey = function Sankey(props) {
149
158
  return function () {
150
159
  window.removeEventListener("resize", handleResize);
151
160
  };
152
- }, [chartOption, handleResize, getImage, containerReady]);
161
+ }, [chartOption, handleResize, getImageRef, containerReady]);
153
162
 
154
163
  // Clean up resources on unmount
155
164
  useEffect(function () {
@@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from "react";
8
8
  import useContainerReady from "../_hooks/useContainerReady";
9
9
  import styles from "../index.module.scss";
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
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -38,6 +39,13 @@ var Tree = function Tree(props) {
38
39
  return option;
39
40
  }, [seriesData, series]);
40
41
 
42
+ // getImage 只是「出图后把 dataURL 交出去」的出口,既不决定图怎么画,
43
+ // 也不该决定要不要重画。它进依赖数组会让 effect 跟着重跑 → setOption(notMerge)
44
+ // → 图重绘 → 再触发一次 finished → 再回调;消费方把图存进 state 就是死循环。
45
+ // 而且 finished 监听只在首次 init 时注册,闭包里锁死的是首帧的 getImage,
46
+ // 后续换了引用永远调不到 —— 依赖数组写了它也没用,是条死依赖。走 ref 两个问题一起解决。
47
+ var getImageRef = useLatestRef(getImage);
48
+
41
49
  // Callback handling chart resize
42
50
  var handleResize = useCallback(function () {
43
51
  var _chartInstanceRef$cur;
@@ -60,7 +68,8 @@ var Tree = function Tree(props) {
60
68
 
61
69
  // Attach the finished event listener on first initialization
62
70
  chart.on("finished", function () {
63
- getImage === null || getImage === void 0 || getImage(chart.getDataURL({
71
+ var _getImageRef$current;
72
+ (_getImageRef$current = getImageRef.current) === null || _getImageRef$current === void 0 || _getImageRef$current.call(getImageRef, chart.getDataURL({
64
73
  type: "png",
65
74
  pixelRatio: 1,
66
75
  backgroundColor: "#fff"
@@ -84,7 +93,7 @@ var Tree = function Tree(props) {
84
93
  return function () {
85
94
  window.removeEventListener("resize", handleResize);
86
95
  };
87
- }, [chartOption, handleResize, getImage, containerReady]);
96
+ }, [chartOption, handleResize, getImageRef, containerReady]);
88
97
 
89
98
  // Clean up resources on unmount
90
99
  useEffect(function () {
@@ -14,6 +14,7 @@ export function useSelectComposition(_ref) {
14
14
  _useState2 = _slicedToArray(_useState, 2),
15
15
  isComposing = _useState2[0],
16
16
  setComposing = _useState2[1];
17
+ var hasOnSearch = !!onSearch;
17
18
  useEffect(function () {
18
19
  var compositionend = function compositionend() {
19
20
  setComposing(false);
@@ -21,7 +22,7 @@ export function useSelectComposition(_ref) {
21
22
  var compositionstart = function compositionstart() {
22
23
  setComposing(true);
23
24
  };
24
- if (onSearch) {
25
+ if (hasOnSearch) {
25
26
  window.addEventListener("compositionstart", compositionstart);
26
27
  window.addEventListener("compositionend", compositionend);
27
28
  }
@@ -29,7 +30,10 @@ export function useSelectComposition(_ref) {
29
30
  window.removeEventListener("compositionstart", compositionstart);
30
31
  window.removeEventListener("compositionend", compositionend);
31
32
  };
32
- }, [onSearch]);
33
+ // onSearch 在这里只被当作「要不要监听输入法事件」的真值判断,effect 体从不调它。
34
+ // 直接进依赖数组的话,消费方传内联箭头就会每渲染一次拆一次、装一次 window 监听;
35
+ // 只取「有没有传」这个布尔量,从无到有时照样会重新注册。
36
+ }, [hasOnSearch]);
33
37
  return {
34
38
  isComposing: isComposing
35
39
  };
@@ -7,6 +7,7 @@ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" !=
7
7
  function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
8
8
  import { useMutationObserver } from "ahooks";
9
9
  import { useCallback, useEffect, useRef, useState } from "react";
10
+ import { useLatestRef } from "../../../hooks/useLatestRef";
10
11
  var DEFAULT_INTERVAL = 2000;
11
12
  var DEFAULT_SPEED = 25;
12
13
  var SMOOTH_SCROLL_TICK = 10;
@@ -29,6 +30,13 @@ var useAutoScrolling = function useAutoScrolling(props) {
29
30
  autoScrollLoopMode = _props$autoScrollLoop2 === void 0 ? "reset" : _props$autoScrollLoop2,
30
31
  _props$autoScrollingO = props.autoScrollingOffset,
31
32
  autoScrollingOffset = _props$autoScrollingO === void 0 ? 0 : _props$autoScrollingO;
33
+
34
+ // onAutoScrollEndAdd 在这里身兼两职:既是「有没有加载更多」的开关,又是真正去加载的动作。
35
+ // 整个 rAF 循环(loop → 依赖它 → 启停 effect 依赖 loop)都挂在它的引用上,
36
+ // 消费方传内联箭头就会每渲染一次重启一次滚动循环 —— 表格滚到一半被打回起点。
37
+ // 拆成两半:开关看布尔量(从无到有传入时仍会正确接上加载更多),动作取 ref 里的最新引用。
38
+ var onAutoScrollEndAddRef = useLatestRef(onAutoScrollEndAdd);
39
+ var hasOnAutoScrollEndAdd = !!onAutoScrollEndAdd;
32
40
  var validSpeed = autoScrollingSpeed > 0 ? autoScrollingSpeed : DEFAULT_SPEED;
33
41
  var smoothStep = validSpeed * SMOOTH_SCROLL_TICK / 1000;
34
42
  var _useState = useState(false),
@@ -260,11 +268,12 @@ var useAutoScrolling = function useAutoScrolling(props) {
260
268
  }, SMOOTH_SCROLL_TICK);
261
269
  }, [bindHover, checkSeamlessReset, clearSmoothTimer, getBody, interval, isAtBottom, isBodyScrollable, isSeamless, smoothStep]);
262
270
  var runLoadMore = useCallback(function () {
263
- if (!onAutoScrollEndAdd || pendingLoadMoreRef.current) return;
271
+ var loadMore = onAutoScrollEndAddRef.current;
272
+ if (!loadMore || pendingLoadMoreRef.current) return;
264
273
  pendingLoadMoreRef.current = true;
265
274
  phaseRef.current = "loading";
266
275
  var token = ++loadTokenRef.current;
267
- onAutoScrollEndAdd().then(function (hasMore) {
276
+ loadMore().then(function (hasMore) {
268
277
  if (token !== loadTokenRef.current) return;
269
278
  var currentBody = getBody();
270
279
  if (hasMore) {
@@ -284,7 +293,7 @@ var useAutoScrolling = function useAutoScrolling(props) {
284
293
  if (token !== loadTokenRef.current) return;
285
294
  pendingLoadMoreRef.current = false;
286
295
  });
287
- }, [autoScrollLoop, getBody, onAutoScrollEndAdd]);
296
+ }, [autoScrollLoop, getBody, onAutoScrollEndAddRef]);
288
297
  var loop = useCallback(function () {
289
298
  rafRef.current = requestAnimationFrame(loop);
290
299
  if (!autoScrolling || !ready || !(dataSource !== null && dataSource !== void 0 && dataSource.length) || !(ref !== null && ref !== void 0 && ref.current)) {
@@ -316,7 +325,7 @@ var useAutoScrolling = function useAutoScrolling(props) {
316
325
  // In seamless mode the scroll never actually reaches the bottom; just keep scrolling
317
326
  phaseRef.current = "idle";
318
327
  } else if (isAtBottom(body)) {
319
- if (onAutoScrollEndAdd) {
328
+ if (hasOnAutoScrollEndAdd) {
320
329
  runLoadMore();
321
330
  } else {
322
331
  if (autoScrollLoop) {
@@ -362,7 +371,7 @@ var useAutoScrolling = function useAutoScrolling(props) {
362
371
  return;
363
372
  }
364
373
  startSmoothStep(body, targetTop);
365
- }, [autoScrollMode, autoScrollLoop, autoScrolling, bindHover, checkSeamlessReset, dataSource, getBody, getRowHeight, interval, isAtBottom, isBodyScrollable, isSeamless, onAutoScrollEndAdd, ready, ref, runLoadMore, setupSeamlessClones, startSmoothStep]);
374
+ }, [autoScrollMode, autoScrollLoop, autoScrolling, bindHover, checkSeamlessReset, dataSource, getBody, getRowHeight, interval, isAtBottom, isBodyScrollable, isSeamless, hasOnAutoScrollEndAdd, ready, ref, runLoadMore, setupSeamlessClones, startSmoothStep]);
366
375
  useEffect(function () {
367
376
  if (!autoScrolling || !ready || !(dataSource !== null && dataSource !== void 0 && dataSource.length) || !(ref !== null && ref !== void 0 && ref.current)) {
368
377
  cleanup();
@@ -387,7 +396,7 @@ var useAutoScrolling = function useAutoScrolling(props) {
387
396
  cleanupClones();
388
397
  return;
389
398
  }
390
- if (onAutoScrollEndAdd) {
399
+ if (hasOnAutoScrollEndAdd) {
391
400
  // In load-more mode, if scrolling had stopped, resume auto-scrolling when new data arrives
392
401
  if (phaseRef.current === "stopped") {
393
402
  phaseRef.current = autoScrollMode === "row" ? "wait" : "idle";
@@ -400,7 +409,7 @@ var useAutoScrolling = function useAutoScrolling(props) {
400
409
  phaseRef.current = autoScrollMode === "row" ? "wait" : "idle";
401
410
  waitUntilRef.current = autoScrollMode === "row" ? performance.now() + interval : 0;
402
411
  }
403
- }, [autoScrollMode, cleanupClones, dataSource, getBody, interval, isBodyScrollable, isSeamless, onAutoScrollEndAdd]);
412
+ }, [autoScrollMode, cleanupClones, dataSource, getBody, interval, isBodyScrollable, isSeamless, hasOnAutoScrollEndAdd]);
404
413
  useEffect(function () {
405
414
  if (!autoScrolling || !(ref !== null && ref !== void 0 && ref.current)) return;
406
415
  var resizeRaf = null;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 把一个**内容没变、但每次渲染都新建**的对象收敛成**引用恒定**的对象:
3
+ * 浅比较(自有键 + `Object.is` 比值)相等就继续返回上一次那个引用。
4
+ *
5
+ * 用途只有一个:让它可以直接进 `useMemo` / `useEffect` 的依赖数组。
6
+ *
7
+ * React 的依赖数组按 `Object.is` 比引用,所以依赖里只要混进一个「每次渲染都新建
8
+ * 的对象」,memo 就**恒不命中**、effect 就**每渲染必重跑**。组件内部最容易出现
9
+ * 这种对象的地方是 rest 解构:
10
+ *
11
+ * ```tsx
12
+ * const { className, style, ...coreOption } = props; // coreOption 每次都是新对象
13
+ * const option = useMemo(() => ({ ...coreOption }), [coreOption]); // 永远不命中
14
+ * ```
15
+ *
16
+ * 消费方就算把每一个 prop 都 memo 好,也躲不掉——新对象是组件自己建的。
17
+ * 正确写法是让这个对象回到值语义:
18
+ *
19
+ * ```tsx
20
+ * const { className, style, ...restOption } = props;
21
+ * const coreOption = useShallowStable(restOption); // 内容不变 → 引用不变
22
+ * ```
23
+ *
24
+ * 注意这**不是**「缓存结果跳过副作用」:memo 该重算时照样重算、effect 该重跑时
25
+ * 照样重跑——只要 rest 里任何一个值真的变了,浅比较就不相等,新引用立刻透出去。
26
+ *
27
+ * 渲染期间读写 ref 的安全性:并发渲染下被丢弃的那次渲染可能已经把 ref 换成了它
28
+ * 自己的对象,但那个对象与当次 props 浅相等,所以后续渲染拿到的引用**内容永远
29
+ * 与当前 props 一致**,不会读到过期值。
30
+ */
31
+ export declare function useShallowStable<T extends object>(value: T): T;
32
+ export default useShallowStable;
@@ -0,0 +1,54 @@
1
+ import { useRef } from "react";
2
+
3
+ /**
4
+ * 把一个**内容没变、但每次渲染都新建**的对象收敛成**引用恒定**的对象:
5
+ * 浅比较(自有键 + `Object.is` 比值)相等就继续返回上一次那个引用。
6
+ *
7
+ * 用途只有一个:让它可以直接进 `useMemo` / `useEffect` 的依赖数组。
8
+ *
9
+ * React 的依赖数组按 `Object.is` 比引用,所以依赖里只要混进一个「每次渲染都新建
10
+ * 的对象」,memo 就**恒不命中**、effect 就**每渲染必重跑**。组件内部最容易出现
11
+ * 这种对象的地方是 rest 解构:
12
+ *
13
+ * ```tsx
14
+ * const { className, style, ...coreOption } = props; // coreOption 每次都是新对象
15
+ * const option = useMemo(() => ({ ...coreOption }), [coreOption]); // 永远不命中
16
+ * ```
17
+ *
18
+ * 消费方就算把每一个 prop 都 memo 好,也躲不掉——新对象是组件自己建的。
19
+ * 正确写法是让这个对象回到值语义:
20
+ *
21
+ * ```tsx
22
+ * const { className, style, ...restOption } = props;
23
+ * const coreOption = useShallowStable(restOption); // 内容不变 → 引用不变
24
+ * ```
25
+ *
26
+ * 注意这**不是**「缓存结果跳过副作用」:memo 该重算时照样重算、effect 该重跑时
27
+ * 照样重跑——只要 rest 里任何一个值真的变了,浅比较就不相等,新引用立刻透出去。
28
+ *
29
+ * 渲染期间读写 ref 的安全性:并发渲染下被丢弃的那次渲染可能已经把 ref 换成了它
30
+ * 自己的对象,但那个对象与当次 props 浅相等,所以后续渲染拿到的引用**内容永远
31
+ * 与当前 props 一致**,不会读到过期值。
32
+ */
33
+ export function useShallowStable(value) {
34
+ var ref = useRef(value);
35
+ if (!shallowEqual(ref.current, value)) {
36
+ ref.current = value;
37
+ }
38
+ return ref.current;
39
+ }
40
+ function shallowEqual(a, b) {
41
+ if (Object.is(a, b)) return true;
42
+ var aKeys = Object.keys(a);
43
+ var bKeys = Object.keys(b);
44
+ if (aKeys.length !== bKeys.length) return false;
45
+ for (var _i = 0, _aKeys = aKeys; _i < _aKeys.length; _i++) {
46
+ var key = _aKeys[_i];
47
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
48
+ if (!Object.is(a[key], b[key])) {
49
+ return false;
50
+ }
51
+ }
52
+ return true;
53
+ }
54
+ export default useShallowStable;
@@ -1,4 +1,5 @@
1
1
  import { useEffect } from "react";
2
+ import { useLatestRef } from "../../../hooks/useLatestRef";
2
3
  import { useLocation } from "react-router";
3
4
  /**
4
5
  * Handle the top-level-only menu logic
@@ -10,16 +11,22 @@ export var useOnlyLvOneMenu = function useOnlyLvOneMenu(_ref) {
10
11
  setOpenkeys = _ref.setOpenkeys,
11
12
  setMenuKey = _ref.setMenuKey;
12
13
  var location = useLocation();
14
+ // getCurrChildItems 是 effect 体里发出去的通知,本身进依赖数组是这一类死循环的标准配方:
15
+ // 消费方传内联箭头 → 每次渲染新引用 → effect 重跑 → setOpenkeys([item.key]) 每次都是
16
+ // 新数组必定触发重渲染 → 又是新引用 …… 直到 React 抛 Maximum update depth exceeded。
17
+ // 通知取 ref 里的最新引用,依赖数组只留真正的输入。
18
+ var getCurrChildItemsRef = useLatestRef(getCurrChildItems);
13
19
  useEffect(function () {
14
20
  if (onlyLvOneMenu) {
15
21
  var item = items.find(function (i) {
16
22
  return i.key === "/" + location.pathname.split("/").filter(Boolean)[0];
17
23
  });
18
24
  if (item) {
19
- getCurrChildItems === null || getCurrChildItems === void 0 || getCurrChildItems(item.children || []);
25
+ var _getCurrChildItemsRef;
26
+ (_getCurrChildItemsRef = getCurrChildItemsRef.current) === null || _getCurrChildItemsRef === void 0 || _getCurrChildItemsRef.call(getCurrChildItemsRef, item.children || []);
20
27
  setOpenkeys([item.key]);
21
28
  setMenuKey(item.key);
22
29
  }
23
30
  }
24
- }, [items, getCurrChildItems, location, onlyLvOneMenu, setOpenkeys, setMenuKey]);
31
+ }, [items, getCurrChildItemsRef, location, onlyLvOneMenu, setOpenkeys, setMenuKey]);
25
32
  };
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.useChainGraphData = useChainGraphData;
7
7
  var _react = require("react");
8
8
  var _hsuUtils = require("hsu-utils");
9
+ var _useLatestRef = require("../../../hooks/useLatestRef");
9
10
  function useChainGraphData(props) {
10
11
  const {
11
12
  graph,
@@ -18,6 +19,25 @@ function useChainGraphData(props) {
18
19
  } = props;
19
20
  const [lastData, setLastData] = (0, _react.useState)(undefined);
20
21
  const [isLayouting, setIsLayouting] = (0, _react.useState)(true);
22
+
23
+ // 这三个回调都是「交给图实例、由它在后续事件里回调」的出口,不是数据本身。
24
+ // 它们进依赖数组是条死依赖:effect 体被 ObjEqual(lastData, data) 挡住,
25
+ // 引用变了也不会重新 setData —— 图里存的还是首次 setData 时那个闭包,
26
+ // 换了引用永远调不到(stale closure)。改成引用恒定的转发函数:
27
+ // 依赖数组只留真正的输入(graph/data/level/rootLevel),调用时总是取到最新的回调。
28
+ const getImageRef = (0, _useLatestRef.useLatestRef)(getImage);
29
+ const labelRenderRef = (0, _useLatestRef.useLatestRef)(labelRender);
30
+ const onLayoutingChangeRef = (0, _useLatestRef.useLatestRef)(onLayoutingChange);
31
+ const emitImage = (0, _react.useCallback)(img => {
32
+ getImageRef.current?.(img);
33
+ }, [getImageRef]);
34
+ // labelRender 在 ChainGraphServices.setData 里是「传了才改写 label」的真值判断,
35
+ // 所以这里保留「有没有传」的语义:没传就传 undefined,传了才给转发函数。
36
+ const renderLabel = (0, _react.useCallback)(label => labelRenderRef.current(label), [labelRenderRef]);
37
+ const emitLayouting = (0, _react.useCallback)(value => {
38
+ setIsLayouting(value);
39
+ onLayoutingChangeRef.current?.(value);
40
+ }, [onLayoutingChangeRef]);
21
41
  (0, _react.useEffect)(() => {
22
42
  if (graph && !_hsuUtils.Equal.ObjEqual(lastData, data)) {
23
43
  setLastData(data);
@@ -25,15 +45,12 @@ function useChainGraphData(props) {
25
45
  data,
26
46
  level,
27
47
  rootLevel,
28
- getImage,
29
- isLayouting: value => {
30
- setIsLayouting(value);
31
- onLayoutingChange?.(value);
32
- },
33
- labelRender
48
+ getImage: emitImage,
49
+ isLayouting: emitLayouting,
50
+ labelRender: labelRenderRef.current ? renderLabel : undefined
34
51
  });
35
52
  }
36
- }, [graph, data, level, rootLevel, getImage, labelRender, onLayoutingChange, lastData]);
53
+ }, [graph, data, level, rootLevel, lastData, emitImage, emitLayouting, renderLabel, labelRenderRef]);
37
54
  return {
38
55
  isLayouting
39
56
  };
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.useChainGraphLayout = useChainGraphLayout;
7
7
  var _react = require("react");
8
+ var _useLatestRef = require("../../../hooks/useLatestRef");
8
9
  function useChainGraphLayout(props) {
9
10
  const {
10
11
  graph,
@@ -12,9 +13,18 @@ function useChainGraphLayout(props) {
12
13
  rootLevel,
13
14
  getImage
14
15
  } = props;
16
+
17
+ // getImage 只是「布局完成后把截图 dataURL 交出去」的出口,不是布局的输入。
18
+ // 它进依赖数组会让消费方每传一次内联箭头就重新 changeLayout 一次 —— 整张图重排;
19
+ // 消费方再把截图存进 state,就变成「重排 → 出图 → setState → 新引用 → 再重排」的死循环。
20
+ // 用引用恒定的转发函数注册,调用时再取最新的 getImage。
21
+ const getImageRef = (0, _useLatestRef.useLatestRef)(getImage);
22
+ const emitImage = (0, _react.useCallback)(img => {
23
+ getImageRef.current?.(img);
24
+ }, [getImageRef]);
15
25
  (0, _react.useEffect)(() => {
16
26
  if (graph && octopus !== undefined) {
17
- graph.changeLayout(octopus, rootLevel, getImage);
27
+ graph.changeLayout(octopus, rootLevel, emitImage);
18
28
  }
19
- }, [graph, octopus, rootLevel, getImage]);
29
+ }, [graph, octopus, rootLevel, emitImage]);
20
30
  }