@hsu-react/ui 2.5.6 → 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/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/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/package.json +1 -1
- package/src/__tests__/depsScan.ts +62 -15
- package/src/__tests__/freshObjectDeps.test.tsx +380 -0
- package/src/__tests__/freshObjectDepsGuard.test.ts +54 -23
- 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
|
@@ -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,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import React, { ReactElement } from "react";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 2.5.7:`xOptions` 的解构默认值与 `xOptionsRest` 这个 rest 包每渲染都是新对象,
|
|
7
|
+
* 直接进「建表格实例」那条 effect 的依赖数组 → **effect 每渲染必重跑**。
|
|
8
|
+
* 实例本身被 `!sheet` 挡住了不会重建,但 effect 体每次都白跑一遍,
|
|
9
|
+
* 且依赖里挂着的 `xOptionsRest` 永远比不出「配置真的变了」——纯粹的假依赖。
|
|
10
|
+
*
|
|
11
|
+
* 这里单独一个文件,是因为要把 `x-data-spreadsheet` 换成假实现(jsdom 里跑不了真的)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const probe = { effect: 0 };
|
|
15
|
+
(globalThis as Record<string, unknown>).__spreadsheetProbe = probe;
|
|
16
|
+
|
|
17
|
+
vi.mock("react", async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof import("react")>();
|
|
19
|
+
const counters = () =>
|
|
20
|
+
(globalThis as Record<string, unknown>).__spreadsheetProbe as typeof probe;
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
useEffect: (fn: () => unknown, deps: unknown[]) =>
|
|
24
|
+
actual.useEffect(() => {
|
|
25
|
+
counters().effect++;
|
|
26
|
+
return fn() as ReturnType<React.EffectCallback>;
|
|
27
|
+
}, deps),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const construct = vi.fn();
|
|
32
|
+
const loadData = vi.fn();
|
|
33
|
+
|
|
34
|
+
vi.mock("x-data-spreadsheet", () => ({
|
|
35
|
+
default: class FakeSpreadsheet {
|
|
36
|
+
constructor(...args: unknown[]) {
|
|
37
|
+
construct(...args);
|
|
38
|
+
}
|
|
39
|
+
loadData(...args: unknown[]) {
|
|
40
|
+
loadData(...args);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
vi.mock("x-data-spreadsheet/dist/xspreadsheet.css", () => ({}));
|
|
46
|
+
|
|
47
|
+
import Spreadsheet from ".";
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
probe.effect = 0;
|
|
51
|
+
construct.mockClear();
|
|
52
|
+
loadData.mockClear();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function rerenderEffectCost(make: () => ReactElement) {
|
|
56
|
+
const { rerender, unmount } = render(make());
|
|
57
|
+
const before = probe.effect;
|
|
58
|
+
rerender(make());
|
|
59
|
+
const cost = probe.effect - before;
|
|
60
|
+
unmount();
|
|
61
|
+
return cost;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("Spreadsheet:xOptions / xOptionsRest 不再让建表 effect 每渲染必重跑", () => {
|
|
65
|
+
it("配置没变时重渲染不再重跑任何 effect", () => {
|
|
66
|
+
// 不传 xOptions(走默认值)与传内联对象(消费方最常见的写法)都必须为 0
|
|
67
|
+
expect(rerenderEffectCost(() => <Spreadsheet />)).toBe(0);
|
|
68
|
+
expect(
|
|
69
|
+
rerenderEffectCost(() => <Spreadsheet xOptions={{ showBottomTool: true }} />),
|
|
70
|
+
).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("表格实例只建一次,重渲染不会重复 new", () => {
|
|
74
|
+
const { rerender } = render(<Spreadsheet />);
|
|
75
|
+
rerender(<Spreadsheet />);
|
|
76
|
+
rerender(<Spreadsheet />);
|
|
77
|
+
expect(construct).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("xOptions 真变了仍然透进新配置(浅比较不相等就立刻透出)", () => {
|
|
81
|
+
// 首帧就带上配置,确认它确实进了 XSpreadsheet 的构造参数
|
|
82
|
+
render(<Spreadsheet xOptions={{ showBottomTool: false, mode: "read" }} />);
|
|
83
|
+
expect(construct).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(construct.mock.calls[0][1]).toMatchObject({ mode: "read" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("data 真变了仍然重新 loadData", () => {
|
|
88
|
+
const bookA = { SheetNames: ["A"], Sheets: { A: {} } } as never;
|
|
89
|
+
const bookB = { SheetNames: ["B"], Sheets: { B: {} } } as never;
|
|
90
|
+
const { rerender } = render(<Spreadsheet data={bookA} />);
|
|
91
|
+
const first = loadData.mock.calls.length;
|
|
92
|
+
rerender(<Spreadsheet data={bookB} />);
|
|
93
|
+
expect(loadData.mock.calls.length).toBeGreaterThan(first);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -6,6 +6,7 @@ import "x-data-spreadsheet/dist/xspreadsheet.css";
|
|
|
6
6
|
import classNames from "classnames";
|
|
7
7
|
import { WorkBook } from "xlsx";
|
|
8
8
|
import { stox } from "./xlsxspread";
|
|
9
|
+
import useShallowStable from "../../hooks/useShallowStable";
|
|
9
10
|
|
|
10
11
|
interface XOptions extends Omit<Options, "view"> {
|
|
11
12
|
showBottomTool?: boolean;
|
|
@@ -17,9 +18,15 @@ interface SpreadsheetProps {
|
|
|
17
18
|
className?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// 解构默认值写成字面量会每次渲染新建一个对象,进依赖数组就让 effect 每渲染必重跑;提到模块级常量
|
|
22
|
+
const EMPTY_X_OPTIONS: XOptions = {};
|
|
23
|
+
|
|
20
24
|
const Spreadsheet: React.FC<SpreadsheetProps> = (props) => {
|
|
21
|
-
const { data, xOptions =
|
|
22
|
-
const { showBottomTool = true, ...
|
|
25
|
+
const { data, xOptions = EMPTY_X_OPTIONS, className } = props;
|
|
26
|
+
const { showBottomTool = true, ...restXOptions } = xOptions;
|
|
27
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 memo 恒不命中。
|
|
28
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
29
|
+
const xOptionsRest = useShallowStable(restXOptions);
|
|
23
30
|
const ref = useRef<HTMLDivElement>(null);
|
|
24
31
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
25
32
|
const id = useMemo(() => generateRandomStr(10), []);
|
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
import usePermissions from "../../../hooks/usePermissions";
|
|
20
20
|
import { PaginationProps } from "../../Pagination";
|
|
21
21
|
|
|
22
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
23
|
+
const EMPTY_COLUMNS: ColumnsType = [];
|
|
24
|
+
|
|
22
25
|
// measureAlign columns: measured at 14px, consistent with the table body and autoWidth title measurement
|
|
23
26
|
const MEASURE_ALIGN_FONT_SIZE = 14;
|
|
24
27
|
// Canvas measureText only returns the advance width, about 1px narrower than the actual DOM rendering; add a 2px buffer to avoid squeezing the widest row
|
|
@@ -97,7 +100,7 @@ const useTableColumns = <T extends AnyObject>(
|
|
|
97
100
|
handleTableChange: TableProps<T>["onChange"];
|
|
98
101
|
} => {
|
|
99
102
|
const {
|
|
100
|
-
columns =
|
|
103
|
+
columns = EMPTY_COLUMNS,
|
|
101
104
|
scroll,
|
|
102
105
|
autoWidth,
|
|
103
106
|
dataSource,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, Key, useMemo, useRef } from "react";
|
|
1
|
+
import { useEffect, useState, Key, useMemo, useRef, useCallback } from "react";
|
|
2
2
|
import { Equal } from "hsu-utils";
|
|
3
3
|
import { CheckedKeys, TreeData } from "..";
|
|
4
4
|
import { normalizeCheckedKeys } from "../_utils/normalizeCheckedKeys";
|
|
@@ -42,13 +42,19 @@ export const useExpandedKeys = (
|
|
|
42
42
|
}
|
|
43
43
|
}, [defaultExpandedKeys, expandedKeysProps]);
|
|
44
44
|
|
|
45
|
-
// Wrap setExpandedKeys to mark that the user has manually interacted
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
// Wrap setExpandedKeys to mark that the user has manually interacted.
|
|
46
|
+
// 必须 useCallback:这个函数被 Tree 的 handleExpand 当依赖用,
|
|
47
|
+
// 每渲染新建一个就让那个 useCallback 恒不命中——展开回调每渲染换引用透给 antd Tree。
|
|
48
|
+
// 函数体只碰 ref 和状态 setter,两者都恒定,所以依赖数组是空的。
|
|
49
|
+
const handleSetExpandedKeys = useCallback(
|
|
50
|
+
(
|
|
51
|
+
keys: Key[] | undefined | ((prev: Key[] | undefined) => Key[] | undefined)
|
|
52
|
+
) => {
|
|
53
|
+
hasUserInteractedRef.current = true;
|
|
54
|
+
setExpandedKeys(keys);
|
|
55
|
+
},
|
|
56
|
+
[]
|
|
57
|
+
);
|
|
52
58
|
|
|
53
59
|
return [expandedKeys, handleSetExpandedKeys] as const;
|
|
54
60
|
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 2.5.7:`treeConfig` 是 rest 解构包,每次渲染都是新对象。它进了 `handleExpand`
|
|
7
|
+
* 的 `useCallback` 依赖数组 → **展开回调每渲染都换一个新引用**,原样传给 antd Tree。
|
|
8
|
+
*
|
|
9
|
+
* 这里把 antd 的 `Tree` 换成一个只记录 props 的探针,直接比 `onExpand` 的引用。
|
|
10
|
+
* 单独一个文件,是因为 `vi.mock` 是文件级的,不能污染别的用例。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const seen: Record<string, unknown>[] = [];
|
|
14
|
+
|
|
15
|
+
vi.mock("antd", async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal<typeof import("antd")>();
|
|
17
|
+
const Probe = (props: Record<string, unknown>) => {
|
|
18
|
+
seen.push(props);
|
|
19
|
+
return React.createElement(actual.Tree, props);
|
|
20
|
+
};
|
|
21
|
+
Probe.DirectoryTree = actual.Tree.DirectoryTree;
|
|
22
|
+
Probe.TreeNode = actual.Tree.TreeNode;
|
|
23
|
+
return { ...actual, Tree: Probe };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
import Tree from ".";
|
|
27
|
+
|
|
28
|
+
const NODES = [{ key: "1", value: "1", title: "节点甲" }];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
seen.length = 0;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("Tree:treeConfig 不再让展开回调每渲染换引用", () => {
|
|
35
|
+
it("props 没变时,传给 antd Tree 的 onExpand 保持同一个引用", () => {
|
|
36
|
+
const make = () => <Tree treeData={NODES} />;
|
|
37
|
+
const { rerender } = render(make());
|
|
38
|
+
rerender(make());
|
|
39
|
+
expect(seen.length).toBeGreaterThanOrEqual(2);
|
|
40
|
+
expect(seen[seen.length - 1].onExpand).toBe(seen[0].onExpand);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("消费方传内联的透传配置时也保持同一个引用(浅比较看的是值)", () => {
|
|
44
|
+
const make = () => <Tree treeData={NODES} showLine blockNode />;
|
|
45
|
+
const { rerender } = render(make());
|
|
46
|
+
rerender(make());
|
|
47
|
+
expect(seen[seen.length - 1].onExpand).toBe(seen[0].onExpand);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("透传配置真变了仍然透出新引用,且新的 onExpand 调的是新的回调", () => {
|
|
51
|
+
const first = vi.fn();
|
|
52
|
+
const second = vi.fn();
|
|
53
|
+
const { rerender } = render(<Tree treeData={NODES} onExpand={first} />);
|
|
54
|
+
type Expand = (keys: unknown[], info: unknown) => void;
|
|
55
|
+
const before = seen[seen.length - 1].onExpand as Expand;
|
|
56
|
+
rerender(<Tree treeData={NODES} onExpand={second} />);
|
|
57
|
+
const after = seen[seen.length - 1].onExpand as Expand;
|
|
58
|
+
expect(after).not.toBe(before);
|
|
59
|
+
|
|
60
|
+
after(["1"], { node: { key: "1" } });
|
|
61
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(first).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -21,6 +21,7 @@ import { ButtonProps } from "../Button";
|
|
|
21
21
|
import { getExpandedKeysByLevel, getNodePath } from "./_utils";
|
|
22
22
|
import TextEllipsis from "../TextEllipsis";
|
|
23
23
|
import usePermissions from "../../hooks/usePermissions";
|
|
24
|
+
import useShallowStable from "../../hooks/useShallowStable";
|
|
24
25
|
import { isLegacyHasSelectorBrowser } from "../../utils/cssSupports";
|
|
25
26
|
|
|
26
27
|
export interface TreeData extends Record<string, unknown> {
|
|
@@ -79,6 +80,9 @@ interface TreeFC extends React.FC<TreeProps> {
|
|
|
79
80
|
TreeNode: typeof AntdTree.TreeNode;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
84
|
+
const EMPTY_TREE_DATA: TreeData[] = [];
|
|
85
|
+
|
|
82
86
|
const Tree = ((props: TreeProps) => {
|
|
83
87
|
const {
|
|
84
88
|
title,
|
|
@@ -88,7 +92,7 @@ const Tree = ((props: TreeProps) => {
|
|
|
88
92
|
titleClassName,
|
|
89
93
|
className,
|
|
90
94
|
search,
|
|
91
|
-
treeData =
|
|
95
|
+
treeData = EMPTY_TREE_DATA,
|
|
92
96
|
onChange,
|
|
93
97
|
onCheck,
|
|
94
98
|
onSelect,
|
|
@@ -109,8 +113,11 @@ const Tree = ((props: TreeProps) => {
|
|
|
109
113
|
onSelectPath,
|
|
110
114
|
allowDeselect = true,
|
|
111
115
|
titleSearchBarClassName,
|
|
112
|
-
...
|
|
116
|
+
...restTreeConfig
|
|
113
117
|
} = props;
|
|
118
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 memo 恒不命中。
|
|
119
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
120
|
+
const treeConfig = useShallowStable(restTreeConfig);
|
|
114
121
|
const { permitted } = usePermissions(hasPermi);
|
|
115
122
|
const legacyHasSelector = isLegacyHasSelectorBrowser();
|
|
116
123
|
|