@hsu-react/ui 2.4.2 → 2.4.3

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.
@@ -14,7 +14,7 @@ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o =
14
14
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
15
15
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
16
16
  function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
17
- import { useCallback, useEffect, useState } from "react";
17
+ import { useCallback, useEffect, useMemo, useState } from "react";
18
18
  import { useLocation } from "react-router";
19
19
  import { checkTabPathMatch } from "../_utils/pathMatch";
20
20
  import { array_is_includes } from "hsu-utils";
@@ -25,6 +25,19 @@ export var useTabPath = function useTabPath(_ref) {
25
25
  var items = _ref.items,
26
26
  affixRouter = _ref.affixRouter;
27
27
  var location = useLocation();
28
+
29
+ /**
30
+ * Keyed on the *contents* of `affixRouter`, not on the array itself.
31
+ *
32
+ * Callers pass an inline literal (`affixRouter={[HOME]}`), and omitting the prop hands the
33
+ * component a fresh `[]` default on every render — either way the array identity changes each
34
+ * time. Depending on it directly re-runs the effect below on every render, and that effect
35
+ * calls setState, so the whole thing becomes a render loop.
36
+ */
37
+ var affixKey = affixRouter.join("\x01");
38
+ var affixSet = useMemo(function () {
39
+ return new Set(affixKey ? affixKey.split("\x01") : []);
40
+ }, [affixKey]);
28
41
  var _useState = useState(""),
29
42
  _useState2 = _slicedToArray(_useState, 2),
30
43
  tabKey = _useState2[0],
@@ -43,7 +56,7 @@ export var useTabPath = function useTabPath(_ref) {
43
56
  _checkAffix(item.children);
44
57
  } else {
45
58
  // Check whether it is an affixed route
46
- var isAffix = affixRouter.includes(item.key) || item.affix;
59
+ var isAffix = affixSet.has(item.key) || item.affix;
47
60
  if (isAffix) {
48
61
  setOpenkeys(function (prev) {
49
62
  var find = prev.find(function (i) {
@@ -57,7 +70,7 @@ export var useTabPath = function useTabPath(_ref) {
57
70
  }
58
71
  }
59
72
  });
60
- }, [affixRouter]);
73
+ }, [affixSet]);
61
74
  var _checkPath = useCallback(function (items, parents) {
62
75
  var pathname = decodeURI(location.pathname);
63
76
  var search = location.search;
@@ -79,36 +92,48 @@ export var useTabPath = function useTabPath(_ref) {
79
92
  // Handle param routes
80
93
  setTabKey("".concat(pathname).concat(search));
81
94
  setOpenkeys(function (prev) {
95
+ var nextKey = "".concat(pathname).concat(search);
82
96
  var find = prev.find(function (i) {
83
97
  return i.key.split("?")[0] === pathname;
84
98
  });
85
99
  if (find) {
100
+ // Already on this key: return `prev` untouched. `map` builds a new array
101
+ // every time, and React treats a new reference as a state change — which is
102
+ // the other half of the render loop above.
103
+ if (find.key === nextKey) {
104
+ return prev;
105
+ }
86
106
  return prev.map(function (i) {
87
107
  return i.key.split("?")[0] === pathname ? _objectSpread(_objectSpread({}, i), {}, {
88
- key: "".concat(pathname).concat(search)
108
+ key: nextKey
89
109
  }) : i;
90
110
  });
91
111
  }
92
112
  return [].concat(_toConsumableArray(prev), [_objectSpread(_objectSpread({}, item), {}, {
93
- key: "".concat(pathname).concat(search)
113
+ key: nextKey
94
114
  })]);
95
115
  });
96
116
  } else {
97
117
  // Handle plain routes
98
118
  setTabKey("".concat(item.key).concat(search));
99
119
  setOpenkeys(function (prev) {
120
+ var nextKey = "".concat(item.key).concat(search);
100
121
  var find = prev.find(function (i) {
101
122
  return i.key.split("?")[0] === item.key;
102
123
  });
103
124
  if (find) {
125
+ // Same as above: an unchanged tab must not produce a new reference
126
+ if (find.key === nextKey) {
127
+ return prev;
128
+ }
104
129
  return prev.map(function (i) {
105
130
  return i.key.split("?")[0] === item.key ? _objectSpread(_objectSpread({}, i), {}, {
106
- key: "".concat(item.key).concat(search)
131
+ key: nextKey
107
132
  }) : i;
108
133
  });
109
134
  }
110
135
  return [].concat(_toConsumableArray(prev), [_objectSpread(_objectSpread({}, item), {}, {
111
- key: "".concat(item.key).concat(search)
136
+ key: nextKey
112
137
  })]);
113
138
  });
114
139
  }
@@ -116,12 +141,24 @@ export var useTabPath = function useTabPath(_ref) {
116
141
  }
117
142
  });
118
143
  }, [location.pathname, location.search]);
144
+
145
+ /**
146
+ * Closing the last tab navigates to `basePath` — but when that tab *was* `basePath`, the
147
+ * location never changes, so path matching would not re-run and the bar would be left empty
148
+ * while its page is still on screen. Going empty has to re-trigger the match on its own.
149
+ *
150
+ * This settles rather than looping: re-matching adds the tab back, which flips `isEmpty` and
151
+ * runs the effect once more, and that pass finds the tab already present and returns the same
152
+ * state object — so React stops there. (Only true because the setters below bail out when
153
+ * nothing changed; without that this would spin.)
154
+ */
155
+ var isEmpty = openKeys.length === 0;
119
156
  useEffect(function () {
120
157
  // Handle affixed routes first to populate openKeys
121
158
  _checkAffix(items);
122
159
  // Then run path matching
123
160
  _checkPath(items);
124
- }, [_checkAffix, _checkPath, items]);
161
+ }, [_checkAffix, _checkPath, items, isEmpty]);
125
162
  return {
126
163
  tabKey: tabKey,
127
164
  setTabKey: setTabKey,
@@ -16,6 +16,17 @@ const useTabPath = ({
16
16
  affixRouter
17
17
  }) => {
18
18
  const location = (0, _reactRouter.useLocation)();
19
+
20
+ /**
21
+ * Keyed on the *contents* of `affixRouter`, not on the array itself.
22
+ *
23
+ * Callers pass an inline literal (`affixRouter={[HOME]}`), and omitting the prop hands the
24
+ * component a fresh `[]` default on every render — either way the array identity changes each
25
+ * time. Depending on it directly re-runs the effect below on every render, and that effect
26
+ * calls setState, so the whole thing becomes a render loop.
27
+ */
28
+ const affixKey = affixRouter.join("\u0001");
29
+ const affixSet = (0, _react.useMemo)(() => new Set(affixKey ? affixKey.split("\u0001") : []), [affixKey]);
19
30
  const [tabKey, setTabKey] = (0, _react.useState)("");
20
31
  const [openKeys, setOpenkeys] = (0, _react.useState)([]);
21
32
 
@@ -28,7 +39,7 @@ const useTabPath = ({
28
39
  _checkAffix(item.children);
29
40
  } else {
30
41
  // Check whether it is an affixed route
31
- const isAffix = affixRouter.includes(item.key) || item.affix;
42
+ const isAffix = affixSet.has(item.key) || item.affix;
32
43
  if (isAffix) {
33
44
  setOpenkeys(prev => {
34
45
  const find = prev.find(i => i.key === item.key);
@@ -40,7 +51,7 @@ const useTabPath = ({
40
51
  }
41
52
  }
42
53
  });
43
- }, [affixRouter]);
54
+ }, [affixSet]);
44
55
  const _checkPath = (0, _react.useCallback)((items, parents) => {
45
56
  const pathname = decodeURI(location.pathname);
46
57
  const search = location.search;
@@ -58,32 +69,44 @@ const useTabPath = ({
58
69
  // Handle param routes
59
70
  setTabKey(`${pathname}${search}`);
60
71
  setOpenkeys(prev => {
72
+ const nextKey = `${pathname}${search}`;
61
73
  const find = prev.find(i => i.key.split("?")[0] === pathname);
62
74
  if (find) {
75
+ // Already on this key: return `prev` untouched. `map` builds a new array
76
+ // every time, and React treats a new reference as a state change — which is
77
+ // the other half of the render loop above.
78
+ if (find.key === nextKey) {
79
+ return prev;
80
+ }
63
81
  return prev.map(i => i.key.split("?")[0] === pathname ? {
64
82
  ...i,
65
- key: `${pathname}${search}`
83
+ key: nextKey
66
84
  } : i);
67
85
  }
68
86
  return [...prev, {
69
87
  ...item,
70
- key: `${pathname}${search}`
88
+ key: nextKey
71
89
  }];
72
90
  });
73
91
  } else {
74
92
  // Handle plain routes
75
93
  setTabKey(`${item.key}${search}`);
76
94
  setOpenkeys(prev => {
95
+ const nextKey = `${item.key}${search}`;
77
96
  const find = prev.find(i => i.key.split("?")[0] === item.key);
78
97
  if (find) {
98
+ // Same as above: an unchanged tab must not produce a new reference
99
+ if (find.key === nextKey) {
100
+ return prev;
101
+ }
79
102
  return prev.map(i => i.key.split("?")[0] === item.key ? {
80
103
  ...i,
81
- key: `${item.key}${search}`
104
+ key: nextKey
82
105
  } : i);
83
106
  }
84
107
  return [...prev, {
85
108
  ...item,
86
- key: `${item.key}${search}`
109
+ key: nextKey
87
110
  }];
88
111
  });
89
112
  }
@@ -91,12 +114,24 @@ const useTabPath = ({
91
114
  }
92
115
  });
93
116
  }, [location.pathname, location.search]);
117
+
118
+ /**
119
+ * Closing the last tab navigates to `basePath` — but when that tab *was* `basePath`, the
120
+ * location never changes, so path matching would not re-run and the bar would be left empty
121
+ * while its page is still on screen. Going empty has to re-trigger the match on its own.
122
+ *
123
+ * This settles rather than looping: re-matching adds the tab back, which flips `isEmpty` and
124
+ * runs the effect once more, and that pass finds the tab already present and returns the same
125
+ * state object — so React stops there. (Only true because the setters below bail out when
126
+ * nothing changed; without that this would spin.)
127
+ */
128
+ const isEmpty = openKeys.length === 0;
94
129
  (0, _react.useEffect)(() => {
95
130
  // Handle affixed routes first to populate openKeys
96
131
  _checkAffix(items);
97
132
  // Then run path matching
98
133
  _checkPath(items);
99
- }, [_checkAffix, _checkPath, items]);
134
+ }, [_checkAffix, _checkPath, items, isEmpty]);
100
135
  return {
101
136
  tabKey,
102
137
  setTabKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hsu-react/ui",
3
- "version": "2.4.2",
3
+ "version": "2.4.3",
4
4
  "description": "一套基于 React + Ant Design 的中后台业务组件库",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { useLocation } from "react-router";
3
3
  import { TabType } from "..";
4
4
  import { checkTabPathMatch } from "../_utils/pathMatch";
@@ -14,6 +14,17 @@ interface UseTabPathOptions {
14
14
  */
15
15
  export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
16
16
  const location = useLocation();
17
+
18
+ /**
19
+ * Keyed on the *contents* of `affixRouter`, not on the array itself.
20
+ *
21
+ * Callers pass an inline literal (`affixRouter={[HOME]}`), and omitting the prop hands the
22
+ * component a fresh `[]` default on every render — either way the array identity changes each
23
+ * time. Depending on it directly re-runs the effect below on every render, and that effect
24
+ * calls setState, so the whole thing becomes a render loop.
25
+ */
26
+ const affixKey = affixRouter.join("\u0001");
27
+ const affixSet = useMemo(() => new Set(affixKey ? affixKey.split("\u0001") : []), [affixKey]);
17
28
  const [tabKey, setTabKey] = useState<string>("");
18
29
  const [openKeys, setOpenkeys] = useState<TabType[]>([]);
19
30
 
@@ -27,7 +38,7 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
27
38
  _checkAffix(item.children);
28
39
  } else {
29
40
  // Check whether it is an affixed route
30
- const isAffix = affixRouter.includes(item.key) || item.affix;
41
+ const isAffix = affixSet.has(item.key) || item.affix;
31
42
  if (isAffix) {
32
43
  setOpenkeys((prev) => {
33
44
  const find = prev.find((i) => i.key === item.key);
@@ -40,7 +51,7 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
40
51
  }
41
52
  });
42
53
  },
43
- [affixRouter]
54
+ [affixSet]
44
55
  );
45
56
 
46
57
  const _checkPath = useCallback(
@@ -72,32 +83,42 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
72
83
  setTabKey(`${pathname}${search}`);
73
84
 
74
85
  setOpenkeys((prev) => {
86
+ const nextKey = `${pathname}${search}`;
75
87
  const find = prev.find((i) => i.key.split("?")[0] === pathname);
76
88
  if (find) {
89
+ // Already on this key: return `prev` untouched. `map` builds a new array
90
+ // every time, and React treats a new reference as a state change — which is
91
+ // the other half of the render loop above.
92
+ if (find.key === nextKey) {
93
+ return prev;
94
+ }
77
95
  return prev.map((i) =>
78
- i.key.split("?")[0] === pathname
79
- ? { ...i, key: `${pathname}${search}` }
80
- : i
96
+ i.key.split("?")[0] === pathname ? { ...i, key: nextKey } : i
81
97
  );
82
98
  }
83
99
 
84
- return [...prev, { ...item, key: `${pathname}${search}` }];
100
+ return [...prev, { ...item, key: nextKey }];
85
101
  });
86
102
  } else {
87
103
  // Handle plain routes
88
104
  setTabKey(`${item.key}${search}`);
89
105
 
90
106
  setOpenkeys((prev) => {
107
+ const nextKey = `${item.key}${search}`;
91
108
  const find = prev.find((i) => i.key.split("?")[0] === item.key);
92
109
  if (find) {
110
+ // Same as above: an unchanged tab must not produce a new reference
111
+ if (find.key === nextKey) {
112
+ return prev;
113
+ }
93
114
  return prev.map((i) =>
94
115
  i.key.split("?")[0] === item.key
95
- ? { ...i, key: `${item.key}${search}` }
116
+ ? { ...i, key: nextKey }
96
117
  : i
97
118
  );
98
119
  }
99
120
 
100
- return [...prev, { ...item, key: `${item.key}${search}` }];
121
+ return [...prev, { ...item, key: nextKey }];
101
122
  });
102
123
  }
103
124
  }
@@ -107,12 +128,24 @@ export const useTabPath = ({ items, affixRouter }: UseTabPathOptions) => {
107
128
  [location.pathname, location.search]
108
129
  );
109
130
 
131
+ /**
132
+ * Closing the last tab navigates to `basePath` — but when that tab *was* `basePath`, the
133
+ * location never changes, so path matching would not re-run and the bar would be left empty
134
+ * while its page is still on screen. Going empty has to re-trigger the match on its own.
135
+ *
136
+ * This settles rather than looping: re-matching adds the tab back, which flips `isEmpty` and
137
+ * runs the effect once more, and that pass finds the tab already present and returns the same
138
+ * state object — so React stops there. (Only true because the setters below bail out when
139
+ * nothing changed; without that this would spin.)
140
+ */
141
+ const isEmpty = openKeys.length === 0;
142
+
110
143
  useEffect(() => {
111
144
  // Handle affixed routes first to populate openKeys
112
145
  _checkAffix(items);
113
146
  // Then run path matching
114
147
  _checkPath(items);
115
- }, [_checkAffix, _checkPath, items]);
148
+ }, [_checkAffix, _checkPath, items, isEmpty]);
116
149
 
117
150
  return { tabKey, setTabKey, openKeys, setOpenkeys };
118
151
  };