@hsu-react/ui 2.5.5 → 2.5.7
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.
- package/es/components/Chart/Bar/index.js +6 -1
- package/es/components/Chart/Bubble/index.js +6 -1
- package/es/components/Chart/Common/index.js +6 -1
- package/es/components/Chart/Gauge/index.js +6 -1
- package/es/components/Chart/Heatmap/index.js +9 -2
- package/es/components/Chart/Line/index.js +6 -1
- package/es/components/Chart/Pie/Pie3D/index.js +15 -7
- package/es/components/Chart/Pie/index.js +9 -2
- package/es/components/Chart/Polar/index.js +9 -2
- package/es/components/Chart/Radar/index.js +6 -1
- package/es/components/Checkbox/CheckboxGroup/index.js +4 -1
- package/es/components/FormItem/FormCodeMirror/index.js +3 -1
- package/es/components/FormItem/ItemContainer/index.js +15 -5
- package/es/components/Operate/index.js +4 -1
- package/es/components/Pagination/index.js +3 -1
- package/es/components/Search/_hooks/useSearchCommon.js +4 -1
- package/es/components/Select/AutoCompleteSelect/index.js +3 -1
- package/es/components/Spreadsheet/index.js +8 -2
- package/es/components/Table/_hooks/useTableColumns.js +4 -1
- package/es/components/Tree/_hooks/useTreeState.js +7 -4
- package/es/components/Tree/index.js +8 -2
- package/es/hooks/useShallowStable.d.ts +32 -0
- package/es/hooks/useShallowStable.js +54 -0
- package/lib/components/Chart/Bar/index.js +6 -1
- package/lib/components/Chart/Bubble/index.js +6 -1
- package/lib/components/Chart/Common/index.js +6 -1
- package/lib/components/Chart/Gauge/index.js +6 -1
- package/lib/components/Chart/Heatmap/index.js +9 -2
- package/lib/components/Chart/Line/index.js +6 -1
- package/lib/components/Chart/Pie/Pie3D/index.js +15 -7
- package/lib/components/Chart/Pie/index.js +9 -2
- package/lib/components/Chart/Polar/index.js +9 -2
- package/lib/components/Chart/Radar/index.js +6 -1
- package/lib/components/Checkbox/CheckboxGroup/index.js +4 -1
- package/lib/components/FormItem/FormCodeMirror/index.js +3 -1
- package/lib/components/FormItem/ItemContainer/index.js +16 -6
- package/lib/components/Operate/index.js +4 -1
- package/lib/components/Pagination/index.js +3 -1
- package/lib/components/Search/_hooks/useSearchCommon.js +4 -1
- package/lib/components/Select/AutoCompleteSelect/index.js +3 -1
- package/lib/components/Spreadsheet/index.js +8 -2
- package/lib/components/Table/_hooks/useTableColumns.js +4 -1
- package/lib/components/Tree/_hooks/useTreeState.js +6 -3
- package/lib/components/Tree/index.js +8 -2
- package/lib/hooks/useShallowStable.d.ts +32 -0
- package/lib/hooks/useShallowStable.js +59 -0
- package/package.json +1 -1
- package/src/__tests__/callbackDepsGuard.test.ts +4 -51
- package/src/__tests__/depsScan.ts +205 -0
- package/src/__tests__/freshObjectDeps.test.tsx +380 -0
- package/src/__tests__/freshObjectDepsGuard.test.ts +175 -0
- package/src/components/Chart/Bar/index.tsx +6 -1
- package/src/components/Chart/Bubble/index.tsx +6 -1
- package/src/components/Chart/Common/index.tsx +6 -1
- package/src/components/Chart/Gauge/index.tsx +6 -1
- package/src/components/Chart/Heatmap/index.tsx +20 -12
- package/src/components/Chart/Line/index.tsx +6 -1
- package/src/components/Chart/Pie/Pie3D/index.tsx +12 -3
- package/src/components/Chart/Pie/index.tsx +10 -2
- package/src/components/Chart/Polar/index.tsx +10 -2
- package/src/components/Chart/Radar/index.tsx +6 -1
- package/src/components/Chart/optionMemo.test.tsx +232 -0
- package/src/components/Checkbox/CheckboxGroup/index.tsx +4 -1
- package/src/components/FormItem/FormCodeMirror/index.tsx +4 -1
- package/src/components/FormItem/ItemContainer/index.tsx +21 -6
- package/src/components/Operate/index.tsx +4 -1
- package/src/components/Pagination/index.tsx +4 -1
- package/src/components/Search/_hooks/useSearchCommon.ts +4 -1
- package/src/components/Select/AutoCompleteSelect/index.tsx +4 -1
- package/src/components/Spreadsheet/depsMemo.test.tsx +95 -0
- package/src/components/Spreadsheet/index.tsx +9 -2
- package/src/components/Table/_hooks/useTableColumns.tsx +4 -1
- package/src/components/Tree/_hooks/useTreeState.ts +14 -8
- package/src/components/Tree/depsMemo.test.tsx +64 -0
- package/src/components/Tree/index.tsx +9 -2
- package/src/hooks/useShallowStable.ts +65 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { resolveChartChrome } from "../_utils/chartTheme";
|
|
11
11
|
import useIsDark from "../../../hooks/useIsDark";
|
|
12
12
|
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
13
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
13
14
|
|
|
14
15
|
export interface HeatmapDataItem {
|
|
15
16
|
/** X-axis index or name */
|
|
@@ -43,6 +44,19 @@ export interface ChartHeatmapProps extends ChartCommonProps {
|
|
|
43
44
|
inRangeColor?: string[];
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
48
|
+
const DEFAULT_IN_RANGE_COLOR = [
|
|
49
|
+
"#8BCCFB",
|
|
50
|
+
"#75C3FB",
|
|
51
|
+
"#5AB5F6",
|
|
52
|
+
"#42AEFA",
|
|
53
|
+
"#2DA1F5",
|
|
54
|
+
"#0F89E1",
|
|
55
|
+
"#056FBB",
|
|
56
|
+
"#025794",
|
|
57
|
+
"#0A4089",
|
|
58
|
+
];
|
|
59
|
+
|
|
46
60
|
const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
47
61
|
const {
|
|
48
62
|
className,
|
|
@@ -61,19 +75,13 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
61
75
|
onChart,
|
|
62
76
|
onClick,
|
|
63
77
|
zeroColor,
|
|
64
|
-
inRangeColor =
|
|
65
|
-
|
|
66
|
-
"#75C3FB",
|
|
67
|
-
"#5AB5F6",
|
|
68
|
-
"#42AEFA",
|
|
69
|
-
"#2DA1F5",
|
|
70
|
-
"#0F89E1",
|
|
71
|
-
"#056FBB",
|
|
72
|
-
"#025794",
|
|
73
|
-
"#0A4089",
|
|
74
|
-
],
|
|
75
|
-
...coreOption
|
|
78
|
+
inRangeColor = DEFAULT_IN_RANGE_COLOR,
|
|
79
|
+
...restOption
|
|
76
80
|
} = props;
|
|
81
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
82
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
83
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
84
|
+
const coreOption = useShallowStable(restOption);
|
|
77
85
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
78
86
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
79
87
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { resolveChartChrome } from "../_utils/chartTheme";
|
|
26
26
|
import useIsDark from "../../../hooks/useIsDark";
|
|
27
27
|
import useLatestRef from "../../../hooks/useLatestRef";
|
|
28
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
28
29
|
|
|
29
30
|
export interface ChartLineProps extends ChartCommonProps {
|
|
30
31
|
chartTitle?: string;
|
|
@@ -71,8 +72,12 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
71
72
|
enableLegendAutoScroll = false,
|
|
72
73
|
legendVisibleCount = 8,
|
|
73
74
|
legendScrollInterval = 1500,
|
|
74
|
-
...
|
|
75
|
+
...restOption
|
|
75
76
|
} = props;
|
|
77
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
78
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
79
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
80
|
+
const coreOption = useShallowStable(restOption);
|
|
76
81
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
77
82
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
78
83
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -8,6 +8,7 @@ import { useMouseOverHandler, useGlobalOutHandler } from "./_hooks";
|
|
|
8
8
|
import { getPie3DOption } from "./_utils/option";
|
|
9
9
|
import styles from "../../index.module.scss";
|
|
10
10
|
import { useLatestRef } from "../../../../hooks/useLatestRef";
|
|
11
|
+
import useShallowStable from "../../../../hooks/useShallowStable";
|
|
11
12
|
|
|
12
13
|
export interface Pie3DDataItem {
|
|
13
14
|
name: string;
|
|
@@ -86,6 +87,10 @@ export interface SeriesItem {
|
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
// 解构默认值写成字面量会每次渲染新建一个对象,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
91
|
+
const DEFAULT_TOOLTIP = { show: false };
|
|
92
|
+
const DEFAULT_LABEL: LabelConfig = { show: true };
|
|
93
|
+
|
|
89
94
|
const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
|
|
90
95
|
const {
|
|
91
96
|
className,
|
|
@@ -100,13 +105,17 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
|
|
|
100
105
|
minHeight = 1,
|
|
101
106
|
maxHeight = 10,
|
|
102
107
|
yOffset = -0.2,
|
|
103
|
-
tooltip =
|
|
104
|
-
label =
|
|
108
|
+
tooltip = DEFAULT_TOOLTIP,
|
|
109
|
+
label = DEFAULT_LABEL,
|
|
105
110
|
enableMouseControl = false,
|
|
106
111
|
onChart,
|
|
107
112
|
onClick,
|
|
108
|
-
...
|
|
113
|
+
...restOption
|
|
109
114
|
} = props;
|
|
115
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
116
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
117
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
118
|
+
const coreOption = useShallowStable(restOption);
|
|
110
119
|
|
|
111
120
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
112
121
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
@@ -14,6 +14,7 @@ import { autoScrollLegend } from "../chartUtils";
|
|
|
14
14
|
import styles from "../index.module.scss";
|
|
15
15
|
import ChartPie3D, { ChartPie3DProps } from "./Pie3D";
|
|
16
16
|
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
17
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
17
18
|
|
|
18
19
|
export interface ChartPieProps extends ChartCommonProps {
|
|
19
20
|
chartTitle?: string;
|
|
@@ -43,6 +44,9 @@ export interface ChartPieFC extends React.FC<ChartPieProps> {
|
|
|
43
44
|
Three: React.FC<ChartPie3DProps>;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
48
|
+
const EMPTY_EXTEND_SERIES: Series[] = [];
|
|
49
|
+
|
|
46
50
|
const ChartPie: ChartPieFC = (props) => {
|
|
47
51
|
const {
|
|
48
52
|
className,
|
|
@@ -54,7 +58,7 @@ const ChartPie: ChartPieFC = (props) => {
|
|
|
54
58
|
series,
|
|
55
59
|
title,
|
|
56
60
|
onChart,
|
|
57
|
-
extendSeries =
|
|
61
|
+
extendSeries = EMPTY_EXTEND_SERIES,
|
|
58
62
|
onClick,
|
|
59
63
|
enableLegendAutoScroll = false,
|
|
60
64
|
legendVisibleCount = 8,
|
|
@@ -64,8 +68,12 @@ const ChartPie: ChartPieFC = (props) => {
|
|
|
64
68
|
spanAngle = 180,
|
|
65
69
|
center,
|
|
66
70
|
radius,
|
|
67
|
-
...
|
|
71
|
+
...restOption
|
|
68
72
|
} = props;
|
|
73
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
74
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
75
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
76
|
+
const coreOption = useShallowStable(restOption);
|
|
69
77
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
70
78
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
71
79
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "..";
|
|
9
9
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
10
10
|
import styles from "../index.module.scss";
|
|
11
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
11
12
|
|
|
12
13
|
export interface ChartPolarProps extends ChartCommonProps {
|
|
13
14
|
title1?: string;
|
|
@@ -18,6 +19,9 @@ export interface ChartPolarProps extends ChartCommonProps {
|
|
|
18
19
|
color?: string[] | string;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
23
|
+
const DEFAULT_COLOR = ["#00FFA3", "#00FFA3"];
|
|
24
|
+
|
|
21
25
|
const Polar: React.FC<ChartPolarProps> = (props) => {
|
|
22
26
|
const {
|
|
23
27
|
className,
|
|
@@ -32,9 +36,13 @@ const Polar: React.FC<ChartPolarProps> = (props) => {
|
|
|
32
36
|
title2Style,
|
|
33
37
|
textStyle,
|
|
34
38
|
seriesData,
|
|
35
|
-
color =
|
|
36
|
-
...
|
|
39
|
+
color = DEFAULT_COLOR,
|
|
40
|
+
...restOption
|
|
37
41
|
} = props;
|
|
42
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
43
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
44
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
45
|
+
const coreOption = useShallowStable(restOption);
|
|
38
46
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
39
47
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
40
48
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -3,6 +3,7 @@ import useContainerReady from "../_hooks/useContainerReady";
|
|
|
3
3
|
import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
|
|
4
4
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
5
5
|
import styles from "../index.module.scss";
|
|
6
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
6
7
|
|
|
7
8
|
export interface RadarIndicator {
|
|
8
9
|
name: string;
|
|
@@ -51,8 +52,12 @@ const Radar: React.FC<ChartRadarProps> = (props) => {
|
|
|
51
52
|
singleIndicatorTooltip: singleIndicatorTooltipProp = false,
|
|
52
53
|
singleIndicatorMode,
|
|
53
54
|
singleIndicatorHoverFill,
|
|
54
|
-
...
|
|
55
|
+
...restOption
|
|
55
56
|
} = props;
|
|
57
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
58
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
59
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
60
|
+
const coreOption = useShallowStable(restOption);
|
|
56
61
|
|
|
57
62
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
58
63
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { act, render } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 这一组用例盯的是 `chartOption` 那个 `useMemo` 的**依赖必须是值,不能是每次渲染
|
|
7
|
+
* 新建的对象**。
|
|
8
|
+
*
|
|
9
|
+
* 依赖里一旦混进新建对象(`...coreOption` 这个 rest 包、或写在解构默认值里的
|
|
10
|
+
* `= { show: false }` / `= ["#fff"]` 字面量),memo 就**恒不命中**:
|
|
11
|
+
* 父组件每渲染一次 → chartOption 换新引用 → effect 重跑 → `setOption(…, true)`
|
|
12
|
+
* 整个重设配置 —— 动画重播、鼠标悬浮态被清掉,用户直接看得见。
|
|
13
|
+
*
|
|
14
|
+
* 修法不是「让 effect 少跑」,而是让依赖回到值语义:
|
|
15
|
+
* - rest 包走 `useShallowStable`(内容浅相等就复用同一个引用);
|
|
16
|
+
* - 解构默认值里的对象/数组字面量提到模块级常量。
|
|
17
|
+
*
|
|
18
|
+
* 所以每个组件都验两条:
|
|
19
|
+
* 1. 父组件重渲染、props 未变 → 不再 setOption;
|
|
20
|
+
* 2. 配置真变了(换数据)→ 仍然要 setOption。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const chart = {
|
|
24
|
+
setOption: vi.fn(),
|
|
25
|
+
resize: vi.fn(),
|
|
26
|
+
dispose: vi.fn(),
|
|
27
|
+
getDataURL: vi.fn(() => "data:image/png;base64,AAAA"),
|
|
28
|
+
getOption: vi.fn(() => ({ dataZoom: [{ start: 0, end: 100 }] })),
|
|
29
|
+
on: vi.fn(),
|
|
30
|
+
off: vi.fn(),
|
|
31
|
+
dispatchAction: vi.fn(),
|
|
32
|
+
getZr: vi.fn(() => ({ on: vi.fn(), off: vi.fn() })),
|
|
33
|
+
convertToPixel: vi.fn(() => [0, 0]),
|
|
34
|
+
getWidth: vi.fn(() => 600),
|
|
35
|
+
getHeight: vi.fn(() => 400),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
vi.mock("echarts", () => ({
|
|
39
|
+
init: () => chart,
|
|
40
|
+
use: () => {},
|
|
41
|
+
registerTheme: () => {},
|
|
42
|
+
graphic: {
|
|
43
|
+
LinearGradient: class {
|
|
44
|
+
constructor(..._args: unknown[]) {}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
vi.mock("echarts-gl", () => ({}));
|
|
49
|
+
|
|
50
|
+
import ChartBar from "./Bar";
|
|
51
|
+
import ChartLine from "./Line";
|
|
52
|
+
import ChartPie from "./Pie";
|
|
53
|
+
import ChartBubble from "./Bubble";
|
|
54
|
+
import ChartHeatmap from "./Heatmap";
|
|
55
|
+
import ChartPolar from "./Polar";
|
|
56
|
+
import ChartRadar from "./Radar";
|
|
57
|
+
import ChartCommon from "./Common";
|
|
58
|
+
import ChartGauge from "./Gauge";
|
|
59
|
+
import ChartPie3D from "./Pie/Pie3D";
|
|
60
|
+
|
|
61
|
+
// Chart.Bubble 要等 ResizeObserver 报出容器尺寸才会 init,jsdom 的空实现永远不回调,
|
|
62
|
+
// 这里补一个观察即回调的实现
|
|
63
|
+
class FiringResizeObserver {
|
|
64
|
+
constructor(private cb: ResizeObserverCallback) {}
|
|
65
|
+
observe(target: Element) {
|
|
66
|
+
this.cb(
|
|
67
|
+
[
|
|
68
|
+
{
|
|
69
|
+
target,
|
|
70
|
+
contentRect: { width: 600, height: 400 },
|
|
71
|
+
} as unknown as ResizeObserverEntry,
|
|
72
|
+
],
|
|
73
|
+
this as unknown as ResizeObserver,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
unobserve() {}
|
|
77
|
+
disconnect() {}
|
|
78
|
+
}
|
|
79
|
+
(globalThis as { ResizeObserver?: unknown }).ResizeObserver =
|
|
80
|
+
FiringResizeObserver;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
vi.clearAllMocks();
|
|
84
|
+
vi.spyOn(Element.prototype, "getBoundingClientRect").mockReturnValue({
|
|
85
|
+
width: 600,
|
|
86
|
+
height: 400,
|
|
87
|
+
top: 0,
|
|
88
|
+
left: 0,
|
|
89
|
+
right: 600,
|
|
90
|
+
bottom: 400,
|
|
91
|
+
x: 0,
|
|
92
|
+
y: 0,
|
|
93
|
+
toJSON: () => ({}),
|
|
94
|
+
} as DOMRect);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const X_AXIS = ["一", "二", "三", "四"];
|
|
98
|
+
const SERIES = [{ name: "A", value: [1, 2, 3, 4] }];
|
|
99
|
+
const SERIES_2 = [{ name: "A", value: [9, 9, 9, 9] }];
|
|
100
|
+
const PIE_DATA = [{ name: "a", value: 1 }];
|
|
101
|
+
const PIE_DATA_2 = [{ name: "a", value: 2 }];
|
|
102
|
+
const BUBBLE_DATA = [{ name: "a", value: 1 }];
|
|
103
|
+
const BUBBLE_DATA_2 = [{ name: "a", value: 5 }];
|
|
104
|
+
const HEATMAP_DATA: Array<[number, number, number]> = [[0, 0, 1]];
|
|
105
|
+
const HEATMAP_DATA_2: Array<[number, number, number]> = [[0, 0, 7]];
|
|
106
|
+
const RADAR_INDICATORS = [{ name: "a", max: 10 }];
|
|
107
|
+
const RADAR_DATA = [1];
|
|
108
|
+
const RADAR_DATA_2 = [8];
|
|
109
|
+
const POLAR_SERIES = { value: 1, name: "a" };
|
|
110
|
+
const POLAR_SERIES_2 = { value: 8, name: "a" };
|
|
111
|
+
const COMMON_SERIES = [{ type: "bar" as const, data: [1, 2, 3] }];
|
|
112
|
+
const COMMON_SERIES_2 = [{ type: "bar" as const, data: [4, 5, 6] }];
|
|
113
|
+
const GAUGE_DATA = [{ value: 1, name: "a" }];
|
|
114
|
+
const GAUGE_DATA_2 = [{ value: 6, name: "a" }];
|
|
115
|
+
const PIE3D_DATA = [{ name: "a", value: 1 }];
|
|
116
|
+
const PIE3D_DATA_2 = [{ name: "a", value: 4 }];
|
|
117
|
+
|
|
118
|
+
/** 每个组件:[名字, 组件, 稳定 props, 会真正改变配置的 props] */
|
|
119
|
+
const cases: Array<
|
|
120
|
+
[string, React.ComponentType<Record<string, unknown>>, Record<string, unknown>, Record<string, unknown>]
|
|
121
|
+
> = [
|
|
122
|
+
[
|
|
123
|
+
"Chart.Bar",
|
|
124
|
+
ChartBar as React.ComponentType<Record<string, unknown>>,
|
|
125
|
+
{ xAxisData: X_AXIS, seriesData: SERIES },
|
|
126
|
+
{ seriesData: SERIES_2 },
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
"Chart.Line",
|
|
130
|
+
ChartLine as React.ComponentType<Record<string, unknown>>,
|
|
131
|
+
{ xAxisData: X_AXIS, seriesData: SERIES },
|
|
132
|
+
{ seriesData: SERIES_2 },
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
"Chart.Pie",
|
|
136
|
+
ChartPie as React.ComponentType<Record<string, unknown>>,
|
|
137
|
+
{ seriesData: PIE_DATA },
|
|
138
|
+
{ seriesData: PIE_DATA_2 },
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
"Chart.Bubble",
|
|
142
|
+
ChartBubble as React.ComponentType<Record<string, unknown>>,
|
|
143
|
+
{ data: BUBBLE_DATA },
|
|
144
|
+
{ data: BUBBLE_DATA_2 },
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"Chart.Heatmap",
|
|
148
|
+
ChartHeatmap as React.ComponentType<Record<string, unknown>>,
|
|
149
|
+
{ data: HEATMAP_DATA },
|
|
150
|
+
{ data: HEATMAP_DATA_2 },
|
|
151
|
+
],
|
|
152
|
+
[
|
|
153
|
+
"Chart.Polar",
|
|
154
|
+
ChartPolar as React.ComponentType<Record<string, unknown>>,
|
|
155
|
+
{ seriesData: POLAR_SERIES },
|
|
156
|
+
{ seriesData: POLAR_SERIES_2 },
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
"Chart.Radar",
|
|
160
|
+
ChartRadar as React.ComponentType<Record<string, unknown>>,
|
|
161
|
+
{ indicators: RADAR_INDICATORS, data: RADAR_DATA },
|
|
162
|
+
{ data: RADAR_DATA_2 },
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"Chart.Gauge",
|
|
166
|
+
ChartGauge as React.ComponentType<Record<string, unknown>>,
|
|
167
|
+
{ seriesData: GAUGE_DATA },
|
|
168
|
+
{ seriesData: GAUGE_DATA_2 },
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
"Chart.Pie.Three",
|
|
172
|
+
ChartPie3D as React.ComponentType<Record<string, unknown>>,
|
|
173
|
+
{ pieData: PIE3D_DATA },
|
|
174
|
+
{ pieData: PIE3D_DATA_2 },
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"Chart.Common",
|
|
178
|
+
ChartCommon as React.ComponentType<Record<string, unknown>>,
|
|
179
|
+
{ xAxis: { type: "category" as const }, series: COMMON_SERIES },
|
|
180
|
+
{ series: COMMON_SERIES_2 },
|
|
181
|
+
],
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
describe("chartOption 的 useMemo 依赖必须是值", () => {
|
|
185
|
+
for (const [name, Comp, stableProps, changedProps] of cases) {
|
|
186
|
+
describe(name, () => {
|
|
187
|
+
it("父组件重渲染但配置未变 → 不重设 option", () => {
|
|
188
|
+
let bump: () => void = () => {};
|
|
189
|
+
|
|
190
|
+
function Consumer() {
|
|
191
|
+
const [, setTick] = useState(0);
|
|
192
|
+
bump = () => setTick((n) => n + 1);
|
|
193
|
+
|
|
194
|
+
return <Comp {...stableProps} />;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
render(<Consumer />);
|
|
198
|
+
const initial = chart.setOption.mock.calls.length;
|
|
199
|
+
expect(initial).toBeGreaterThan(0);
|
|
200
|
+
|
|
201
|
+
act(() => bump());
|
|
202
|
+
act(() => bump());
|
|
203
|
+
act(() => bump());
|
|
204
|
+
|
|
205
|
+
expect(chart.setOption.mock.calls.length).toBe(initial);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("配置真变了 → 仍然重设 option", () => {
|
|
209
|
+
let change: () => void = () => {};
|
|
210
|
+
|
|
211
|
+
function Consumer() {
|
|
212
|
+
const [changed, setChanged] = useState(false);
|
|
213
|
+
change = () => setChanged(true);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Comp
|
|
217
|
+
{...stableProps}
|
|
218
|
+
{...(changed ? changedProps : {})}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
render(<Consumer />);
|
|
224
|
+
const initial = chart.setOption.mock.calls.length;
|
|
225
|
+
|
|
226
|
+
act(() => change());
|
|
227
|
+
|
|
228
|
+
expect(chart.setOption.mock.calls.length).toBeGreaterThan(initial);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
});
|
|
@@ -9,6 +9,9 @@ import styles from "./index.module.scss";
|
|
|
9
9
|
|
|
10
10
|
const Group = Checkbox.Group;
|
|
11
11
|
|
|
12
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
13
|
+
const EMPTY_OPTIONS: AntdCheckboxGroupProps["options"] = [];
|
|
14
|
+
|
|
12
15
|
export interface CheckboxGroupProps extends AntdCheckboxGroupProps {
|
|
13
16
|
outline?: boolean;
|
|
14
17
|
hasAll?: boolean;
|
|
@@ -21,7 +24,7 @@ const CheckboxGroup: React.FC<CheckboxGroupProps> = (props) => {
|
|
|
21
24
|
className,
|
|
22
25
|
outline,
|
|
23
26
|
hasAll,
|
|
24
|
-
options =
|
|
27
|
+
options = EMPTY_OPTIONS,
|
|
25
28
|
value,
|
|
26
29
|
onChange,
|
|
27
30
|
layout = "horizontal",
|
|
@@ -7,12 +7,15 @@ export interface FormCodeMirrorProps extends ItemContainerProps {
|
|
|
7
7
|
componentProps?: CodeMirrorProps;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
11
|
+
const EMPTY_RULES: ItemContainerProps["rules"] = [];
|
|
12
|
+
|
|
10
13
|
const FormCodeMirror: React.FC<FormCodeMirrorProps> = (props) => {
|
|
11
14
|
const {
|
|
12
15
|
componentProps = {},
|
|
13
16
|
className: itemClassName,
|
|
14
17
|
disabled,
|
|
15
|
-
rules =
|
|
18
|
+
rules = EMPTY_RULES,
|
|
16
19
|
name,
|
|
17
20
|
...formItemProps
|
|
18
21
|
} = props;
|
|
@@ -9,6 +9,7 @@ import usePermissions from "../../../hooks/usePermissions";
|
|
|
9
9
|
import { isLegacyHasSelectorBrowser } from "../../../utils/cssSupports";
|
|
10
10
|
import { generateRandomStr } from "hsu-utils";
|
|
11
11
|
import useLabelSize from "./_hooks/useLabelSize";
|
|
12
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
12
13
|
import useInputSize from "./_hooks/useInputSize";
|
|
13
14
|
|
|
14
15
|
interface TipsProps {
|
|
@@ -50,6 +51,9 @@ export interface ItemContainerProps extends FormItemProps {
|
|
|
50
51
|
visible?: boolean;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// 解构默认值写成字面量会每次渲染新建一个对象,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
55
|
+
const EMPTY_TIPS: NonNullable<ItemContainerProps["tips"]> = {};
|
|
56
|
+
|
|
53
57
|
const ItemContainer: React.FC<ItemContainerProps> = (props) => {
|
|
54
58
|
const {
|
|
55
59
|
children,
|
|
@@ -73,7 +77,7 @@ const ItemContainer: React.FC<ItemContainerProps> = (props) => {
|
|
|
73
77
|
colon,
|
|
74
78
|
label,
|
|
75
79
|
labelRender,
|
|
76
|
-
tips =
|
|
80
|
+
tips = EMPTY_TIPS,
|
|
77
81
|
horizontalAlignment = "start",
|
|
78
82
|
hideRequired = false,
|
|
79
83
|
labelClassName,
|
|
@@ -89,7 +93,18 @@ const ItemContainer: React.FC<ItemContainerProps> = (props) => {
|
|
|
89
93
|
visible: _visible,
|
|
90
94
|
...formItemProps
|
|
91
95
|
} = props;
|
|
92
|
-
const {
|
|
96
|
+
const {
|
|
97
|
+
icon = "material-symbols:help",
|
|
98
|
+
iconClassName,
|
|
99
|
+
...restTipsConfig
|
|
100
|
+
} = tips;
|
|
101
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 memo 恒不命中。
|
|
102
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
103
|
+
const tipsConfig = useShallowStable(restTipsConfig);
|
|
104
|
+
// memo 只需要知道「有没有配 tips」,把对象本身放进依赖数组等于按引用比:
|
|
105
|
+
// 消费方写内联 `tips={{ ... }}`(最常见的写法)就让 label 的 memo 恒不命中。
|
|
106
|
+
// 拆成布尔量后依赖回到值语义。
|
|
107
|
+
const hasTips = Object.keys(tips).length > 0;
|
|
93
108
|
const { permitted } = usePermissions(hasPermi);
|
|
94
109
|
const cls = useMemo(() => generateRandomStr(10), []);
|
|
95
110
|
const legacyHasSelector = isLegacyHasSelectorBrowser();
|
|
@@ -99,7 +114,7 @@ const ItemContainer: React.FC<ItemContainerProps> = (props) => {
|
|
|
99
114
|
return undefined;
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
return
|
|
117
|
+
return hasTips ? (
|
|
103
118
|
<>
|
|
104
119
|
<span className={classNames(styles.labelContent, labelClassName)}>
|
|
105
120
|
<span
|
|
@@ -160,7 +175,7 @@ const ItemContainer: React.FC<ItemContainerProps> = (props) => {
|
|
|
160
175
|
</>
|
|
161
176
|
) : undefined;
|
|
162
177
|
}, [
|
|
163
|
-
|
|
178
|
+
hasTips,
|
|
164
179
|
labelClassName,
|
|
165
180
|
label,
|
|
166
181
|
tipsConfig,
|
|
@@ -225,7 +240,7 @@ const ItemContainer: React.FC<ItemContainerProps> = (props) => {
|
|
|
225
240
|
[styles.hideRequired]: hideRequired,
|
|
226
241
|
[styles[horizontalAlignment]]:
|
|
227
242
|
layout === "horizontal" && horizontalAlignment,
|
|
228
|
-
[styles.hasLabelContent]:
|
|
243
|
+
[styles.hasLabelContent]: hasTips,
|
|
229
244
|
[styles.hideAdditiona]: hideAdditiona,
|
|
230
245
|
[styles.legacyHasLabelExtra]:
|
|
231
246
|
legacyHasSelector &&
|
|
@@ -233,7 +248,7 @@ const ItemContainer: React.FC<ItemContainerProps> = (props) => {
|
|
|
233
248
|
!hideLabel &&
|
|
234
249
|
!!label &&
|
|
235
250
|
!!labelExtra &&
|
|
236
|
-
!
|
|
251
|
+
!hasTips,
|
|
237
252
|
})}
|
|
238
253
|
colon={
|
|
239
254
|
typeof colon === "boolean"
|
|
@@ -28,6 +28,9 @@ export interface OperateProps
|
|
|
28
28
|
moreIcon?: ReactNode | false;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
32
|
+
const EMPTY_MENU: OperateProps[] = [];
|
|
33
|
+
|
|
31
34
|
/**
|
|
32
35
|
* Render an icon
|
|
33
36
|
* @param icon - Icon configuration
|
|
@@ -56,7 +59,7 @@ const Operate: React.FC<OperateProps> = (props) => {
|
|
|
56
59
|
const {
|
|
57
60
|
title,
|
|
58
61
|
className,
|
|
59
|
-
menu =
|
|
62
|
+
menu = EMPTY_MENU,
|
|
60
63
|
icon,
|
|
61
64
|
delete: isDelete,
|
|
62
65
|
popconfirm,
|
|
@@ -20,10 +20,13 @@ export interface PaginationProps
|
|
|
20
20
|
onStaticPaginationChange?: (page: number, pageSize: number) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
24
|
+
const EMPTY_PAGE_SIZE_OPTIONS: number[] = [];
|
|
25
|
+
|
|
23
26
|
const Pagination: React.FC<PaginationProps> = (props) => {
|
|
24
27
|
const {
|
|
25
28
|
simple,
|
|
26
|
-
pageSizeOptions =
|
|
29
|
+
pageSizeOptions = EMPTY_PAGE_SIZE_OPTIONS,
|
|
27
30
|
showSizeChanger,
|
|
28
31
|
onShowSizeChange,
|
|
29
32
|
onChange,
|
|
@@ -76,6 +76,9 @@ export interface UseSearchCommonReturn {
|
|
|
76
76
|
permitted: boolean;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
80
|
+
const EMPTY_SEARCH_ITEMS: FormItemProps[] = [];
|
|
81
|
+
|
|
79
82
|
/**
|
|
80
83
|
* Extracts the common logic shared by all Search components
|
|
81
84
|
*/
|
|
@@ -85,7 +88,7 @@ export const useSearchCommon = (
|
|
|
85
88
|
const {
|
|
86
89
|
form,
|
|
87
90
|
searchItems,
|
|
88
|
-
moreSearchItems =
|
|
91
|
+
moreSearchItems = EMPTY_SEARCH_ITEMS,
|
|
89
92
|
searchData,
|
|
90
93
|
hasPermi,
|
|
91
94
|
beforeButtonGroup,
|
|
@@ -40,6 +40,9 @@ export interface AutoCompleteSelectProps
|
|
|
40
40
|
optionFontSize?: number;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
44
|
+
const EMPTY_OPTIONS: AutoCompleteOption[] = [];
|
|
45
|
+
|
|
43
46
|
const AutoCompleteSelect: React.FC<AutoCompleteSelectProps> = (props) => {
|
|
44
47
|
const {
|
|
45
48
|
prefix,
|
|
@@ -53,7 +56,7 @@ const AutoCompleteSelect: React.FC<AutoCompleteSelectProps> = (props) => {
|
|
|
53
56
|
popupClassName,
|
|
54
57
|
filterOption: customFilterOption,
|
|
55
58
|
onChange,
|
|
56
|
-
options =
|
|
59
|
+
options = EMPTY_OPTIONS,
|
|
57
60
|
placement = "bottomLeft",
|
|
58
61
|
onDropdownVisibleChange,
|
|
59
62
|
popupMatchSelectWidth,
|