@reactuses/core 6.1.10 → 6.1.12

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/dist/index.cjs CHANGED
@@ -113,10 +113,57 @@ const useDeepCompareEffect = (effect, deps)=>{
113
113
  useCustomCompareEffect(effect, deps, lodashEs.isEqual);
114
114
  };
115
115
 
116
+ /**
117
+ * Creates a stable identifier for a BasicTarget that can be safely used in effect dependencies.
118
+ *
119
+ * This hook solves the problem where passing unstable function references like `() => document`
120
+ * would cause infinite re-renders when used directly in effect dependency arrays.
121
+ *
122
+ * @param target - The target element (ref, function, or direct element)
123
+ * @param defaultElement - Default element to use if target is undefined
124
+ * @returns A stable reference that only changes when the actual target element changes
125
+ *
126
+ * @example
127
+ * ```tsx
128
+ * // For ref objects: returns the ref itself (stable)
129
+ * const ref = useRef<HTMLDivElement>(null)
130
+ * const key = useStableTarget(ref) // key === ref (stable)
131
+ *
132
+ * // For functions: returns the resolved actual element
133
+ * const key = useStableTarget(() => document) // key === document (stable)
134
+ *
135
+ * // For direct elements: returns the element itself
136
+ * const key = useStableTarget(divElement) // key === divElement (stable)
137
+ * ```
138
+ */ function useStableTarget(target, defaultElement) {
139
+ const targetRef = React.useRef(target);
140
+ targetRef.current = target;
141
+ // Calculate stable key without memoization
142
+ // For ref objects: return the ref itself (always stable)
143
+ // For functions/direct elements: resolve to the actual element
144
+ let stableKey;
145
+ if (!target) {
146
+ stableKey = defaultElement != null ? defaultElement : null;
147
+ } else if (typeof target === 'object' && 'current' in target) {
148
+ // Ref object - use the ref itself as the stable key
149
+ stableKey = target;
150
+ } else {
151
+ // Function or direct element - resolve to actual element
152
+ stableKey = getTargetElement(target, defaultElement);
153
+ }
154
+ return {
155
+ /** The stable key that can be safely used in effect dependencies */ key: stableKey,
156
+ /** A ref containing the current target (useful for accessing in effects) */ ref: targetRef
157
+ };
158
+ }
159
+
116
160
  function useEventListenerImpl(eventName, handler, element, options = defaultOptions$1) {
117
161
  const savedHandler = useLatest(handler);
118
- const targetElement = getTargetElement(element, defaultWindow);
162
+ const { key: elementKey, ref: elementRef } = useStableTarget(element, defaultWindow);
119
163
  useDeepCompareEffect(()=>{
164
+ // Call getTargetElement inside effect to support ref-based targets
165
+ // (ref.current is null during render, only available in commit phase)
166
+ const targetElement = getTargetElement(elementRef.current, defaultWindow);
120
167
  if (!(targetElement && targetElement.addEventListener)) {
121
168
  return;
122
169
  }
@@ -130,7 +177,7 @@ function useEventListenerImpl(eventName, handler, element, options = defaultOpti
130
177
  };
131
178
  }, [
132
179
  eventName,
133
- targetElement,
180
+ elementKey,
134
181
  options
135
182
  ]);
136
183
  }
@@ -1099,13 +1146,14 @@ const useDropZone = (target, onDrop)=>{
1099
1146
  const useResizeObserver = (target, callback, options = defaultOptions$1)=>{
1100
1147
  const savedCallback = useLatest(callback);
1101
1148
  const observerRef = React.useRef();
1149
+ const { key: targetKey, ref: targetRef } = useStableTarget(target);
1102
1150
  const stop = React.useCallback(()=>{
1103
1151
  if (observerRef.current) {
1104
1152
  observerRef.current.disconnect();
1105
1153
  }
1106
1154
  }, []);
1107
1155
  useDeepCompareEffect(()=>{
1108
- const element = getTargetElement(target);
1156
+ const element = getTargetElement(targetRef.current);
1109
1157
  if (!element) {
1110
1158
  return;
1111
1159
  }
@@ -1113,9 +1161,7 @@ const useResizeObserver = (target, callback, options = defaultOptions$1)=>{
1113
1161
  observerRef.current.observe(element, options);
1114
1162
  return stop;
1115
1163
  }, [
1116
- savedCallback,
1117
- stop,
1118
- target,
1164
+ targetKey,
1119
1165
  options
1120
1166
  ]);
1121
1167
  return stop;
@@ -1226,13 +1272,14 @@ const useElementSize = (target, options = defaultOptions$1)=>{
1226
1272
  const useIntersectionObserver = (target, callback, options = defaultOptions$1)=>{
1227
1273
  const savedCallback = useLatest(callback);
1228
1274
  const observerRef = React.useRef();
1275
+ const { key: targetKey, ref: targetRef } = useStableTarget(target);
1229
1276
  const stop = React.useCallback(()=>{
1230
1277
  if (observerRef.current) {
1231
1278
  observerRef.current.disconnect();
1232
1279
  }
1233
1280
  }, []);
1234
1281
  useDeepCompareEffect(()=>{
1235
- const element = getTargetElement(target);
1282
+ const element = getTargetElement(targetRef.current);
1236
1283
  if (!element) {
1237
1284
  return;
1238
1285
  }
@@ -1240,6 +1287,7 @@ const useIntersectionObserver = (target, callback, options = defaultOptions$1)=>
1240
1287
  observerRef.current.observe(element);
1241
1288
  return stop;
1242
1289
  }, [
1290
+ targetKey,
1243
1291
  options
1244
1292
  ]);
1245
1293
  return stop;
@@ -1303,7 +1351,11 @@ function useSupported(callback, sync = false) {
1303
1351
  const [supported, setSupported] = React.useState(false);
1304
1352
  const effect = sync ? useIsomorphicLayoutEffect : React.useEffect;
1305
1353
  effect(()=>{
1306
- setSupported(Boolean(callback()));
1354
+ try {
1355
+ setSupported(Boolean(callback()));
1356
+ } catch (e) {
1357
+ setSupported(false);
1358
+ }
1307
1359
  }, []);
1308
1360
  return supported;
1309
1361
  }
@@ -1634,7 +1686,7 @@ const initCoord = {
1634
1686
  };
1635
1687
  const useGeolocation = (options = defaultOptions$1)=>{
1636
1688
  const { enableHighAccuracy = true, maximumAge = 30000, timeout = 27000 } = options;
1637
- const isSupported = useSupported(()=>navigator && 'geolocation' in navigator);
1689
+ const isSupported = useSupported(()=>typeof navigator !== 'undefined' && !!navigator.geolocation && typeof navigator.geolocation.getCurrentPosition === 'function' && typeof navigator.geolocation.watchPosition === 'function' && typeof navigator.geolocation.clearWatch === 'function');
1638
1690
  const [coordinates, setCoordinates] = React.useState(initCoord);
1639
1691
  const [locatedAt, setLocatedAt] = React.useState(null);
1640
1692
  const [error, setError] = React.useState(null);
@@ -2345,13 +2397,14 @@ const useMousePressed = (target, options = defaultOptions$1)=>{
2345
2397
  const useMutationObserver = (callback, target, options = defaultOptions$1)=>{
2346
2398
  const callbackRef = useLatest(callback);
2347
2399
  const observerRef = React.useRef();
2400
+ const { key: targetKey, ref: targetRef } = useStableTarget(target);
2348
2401
  const stop = React.useCallback(()=>{
2349
2402
  if (observerRef.current) {
2350
2403
  observerRef.current.disconnect();
2351
2404
  }
2352
2405
  }, []);
2353
2406
  useDeepCompareEffect(()=>{
2354
- const element = getTargetElement(target);
2407
+ const element = getTargetElement(targetRef.current);
2355
2408
  if (!element) {
2356
2409
  return;
2357
2410
  }
@@ -2359,6 +2412,7 @@ const useMutationObserver = (callback, target, options = defaultOptions$1)=>{
2359
2412
  observerRef.current.observe(element, options);
2360
2413
  return stop;
2361
2414
  }, [
2415
+ targetKey,
2362
2416
  options
2363
2417
  ]);
2364
2418
  return stop;
@@ -3173,21 +3227,25 @@ const useSetState = (initialState)=>{
3173
3227
 
3174
3228
  function useSticky(targetElement, { axis = 'y', nav = 0 }, scrollElement) {
3175
3229
  const [isSticky, setSticky] = React.useState(false);
3230
+ const { key: targetKey, ref: targetRef } = useStableTarget(targetElement);
3231
+ const { key: scrollKey, ref: scrollRef } = useStableTarget(scrollElement);
3232
+ const axisRef = useLatest(axis);
3233
+ const navRef = useLatest(nav);
3176
3234
  const { run: scrollHandler } = useThrottleFn(()=>{
3177
- const element = getTargetElement(targetElement);
3235
+ const element = getTargetElement(targetRef.current);
3178
3236
  if (!element) {
3179
3237
  return;
3180
3238
  }
3181
3239
  const rect = element.getBoundingClientRect();
3182
- if (axis === 'y') {
3183
- setSticky((rect == null ? void 0 : rect.top) <= nav);
3240
+ if (axisRef.current === 'y') {
3241
+ setSticky((rect == null ? void 0 : rect.top) <= navRef.current);
3184
3242
  } else {
3185
- setSticky((rect == null ? void 0 : rect.left) <= nav);
3243
+ setSticky((rect == null ? void 0 : rect.left) <= navRef.current);
3186
3244
  }
3187
3245
  }, 50);
3188
3246
  React.useEffect(()=>{
3189
- const element = getTargetElement(targetElement);
3190
- const scrollParent = getTargetElement(scrollElement) || getScrollParent(axis, element);
3247
+ const element = getTargetElement(targetRef.current);
3248
+ const scrollParent = getTargetElement(scrollRef.current) || getScrollParent(axisRef.current, element);
3191
3249
  if (!element || !scrollParent) {
3192
3250
  return;
3193
3251
  }
@@ -3197,9 +3255,8 @@ function useSticky(targetElement, { axis = 'y', nav = 0 }, scrollElement) {
3197
3255
  scrollParent.removeEventListener('scroll', scrollHandler);
3198
3256
  };
3199
3257
  }, [
3200
- axis,
3201
- targetElement,
3202
- scrollElement,
3258
+ targetKey,
3259
+ scrollKey,
3203
3260
  scrollHandler
3204
3261
  ]);
3205
3262
  return [
package/dist/index.mjs CHANGED
@@ -105,10 +105,57 @@ const useDeepCompareEffect = (effect, deps)=>{
105
105
  useCustomCompareEffect(effect, deps, isEqual);
106
106
  };
107
107
 
108
+ /**
109
+ * Creates a stable identifier for a BasicTarget that can be safely used in effect dependencies.
110
+ *
111
+ * This hook solves the problem where passing unstable function references like `() => document`
112
+ * would cause infinite re-renders when used directly in effect dependency arrays.
113
+ *
114
+ * @param target - The target element (ref, function, or direct element)
115
+ * @param defaultElement - Default element to use if target is undefined
116
+ * @returns A stable reference that only changes when the actual target element changes
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * // For ref objects: returns the ref itself (stable)
121
+ * const ref = useRef<HTMLDivElement>(null)
122
+ * const key = useStableTarget(ref) // key === ref (stable)
123
+ *
124
+ * // For functions: returns the resolved actual element
125
+ * const key = useStableTarget(() => document) // key === document (stable)
126
+ *
127
+ * // For direct elements: returns the element itself
128
+ * const key = useStableTarget(divElement) // key === divElement (stable)
129
+ * ```
130
+ */ function useStableTarget(target, defaultElement) {
131
+ const targetRef = useRef(target);
132
+ targetRef.current = target;
133
+ // Calculate stable key without memoization
134
+ // For ref objects: return the ref itself (always stable)
135
+ // For functions/direct elements: resolve to the actual element
136
+ let stableKey;
137
+ if (!target) {
138
+ stableKey = defaultElement != null ? defaultElement : null;
139
+ } else if (typeof target === 'object' && 'current' in target) {
140
+ // Ref object - use the ref itself as the stable key
141
+ stableKey = target;
142
+ } else {
143
+ // Function or direct element - resolve to actual element
144
+ stableKey = getTargetElement(target, defaultElement);
145
+ }
146
+ return {
147
+ /** The stable key that can be safely used in effect dependencies */ key: stableKey,
148
+ /** A ref containing the current target (useful for accessing in effects) */ ref: targetRef
149
+ };
150
+ }
151
+
108
152
  function useEventListenerImpl(eventName, handler, element, options = defaultOptions$1) {
109
153
  const savedHandler = useLatest(handler);
110
- const targetElement = getTargetElement(element, defaultWindow);
154
+ const { key: elementKey, ref: elementRef } = useStableTarget(element, defaultWindow);
111
155
  useDeepCompareEffect(()=>{
156
+ // Call getTargetElement inside effect to support ref-based targets
157
+ // (ref.current is null during render, only available in commit phase)
158
+ const targetElement = getTargetElement(elementRef.current, defaultWindow);
112
159
  if (!(targetElement && targetElement.addEventListener)) {
113
160
  return;
114
161
  }
@@ -122,7 +169,7 @@ function useEventListenerImpl(eventName, handler, element, options = defaultOpti
122
169
  };
123
170
  }, [
124
171
  eventName,
125
- targetElement,
172
+ elementKey,
126
173
  options
127
174
  ]);
128
175
  }
@@ -1091,13 +1138,14 @@ const useDropZone = (target, onDrop)=>{
1091
1138
  const useResizeObserver = (target, callback, options = defaultOptions$1)=>{
1092
1139
  const savedCallback = useLatest(callback);
1093
1140
  const observerRef = useRef();
1141
+ const { key: targetKey, ref: targetRef } = useStableTarget(target);
1094
1142
  const stop = useCallback(()=>{
1095
1143
  if (observerRef.current) {
1096
1144
  observerRef.current.disconnect();
1097
1145
  }
1098
1146
  }, []);
1099
1147
  useDeepCompareEffect(()=>{
1100
- const element = getTargetElement(target);
1148
+ const element = getTargetElement(targetRef.current);
1101
1149
  if (!element) {
1102
1150
  return;
1103
1151
  }
@@ -1105,9 +1153,7 @@ const useResizeObserver = (target, callback, options = defaultOptions$1)=>{
1105
1153
  observerRef.current.observe(element, options);
1106
1154
  return stop;
1107
1155
  }, [
1108
- savedCallback,
1109
- stop,
1110
- target,
1156
+ targetKey,
1111
1157
  options
1112
1158
  ]);
1113
1159
  return stop;
@@ -1218,13 +1264,14 @@ const useElementSize = (target, options = defaultOptions$1)=>{
1218
1264
  const useIntersectionObserver = (target, callback, options = defaultOptions$1)=>{
1219
1265
  const savedCallback = useLatest(callback);
1220
1266
  const observerRef = useRef();
1267
+ const { key: targetKey, ref: targetRef } = useStableTarget(target);
1221
1268
  const stop = useCallback(()=>{
1222
1269
  if (observerRef.current) {
1223
1270
  observerRef.current.disconnect();
1224
1271
  }
1225
1272
  }, []);
1226
1273
  useDeepCompareEffect(()=>{
1227
- const element = getTargetElement(target);
1274
+ const element = getTargetElement(targetRef.current);
1228
1275
  if (!element) {
1229
1276
  return;
1230
1277
  }
@@ -1232,6 +1279,7 @@ const useIntersectionObserver = (target, callback, options = defaultOptions$1)=>
1232
1279
  observerRef.current.observe(element);
1233
1280
  return stop;
1234
1281
  }, [
1282
+ targetKey,
1235
1283
  options
1236
1284
  ]);
1237
1285
  return stop;
@@ -1295,7 +1343,11 @@ function useSupported(callback, sync = false) {
1295
1343
  const [supported, setSupported] = useState(false);
1296
1344
  const effect = sync ? useIsomorphicLayoutEffect : useEffect;
1297
1345
  effect(()=>{
1298
- setSupported(Boolean(callback()));
1346
+ try {
1347
+ setSupported(Boolean(callback()));
1348
+ } catch (e) {
1349
+ setSupported(false);
1350
+ }
1299
1351
  }, []);
1300
1352
  return supported;
1301
1353
  }
@@ -1626,7 +1678,7 @@ const initCoord = {
1626
1678
  };
1627
1679
  const useGeolocation = (options = defaultOptions$1)=>{
1628
1680
  const { enableHighAccuracy = true, maximumAge = 30000, timeout = 27000 } = options;
1629
- const isSupported = useSupported(()=>navigator && 'geolocation' in navigator);
1681
+ const isSupported = useSupported(()=>typeof navigator !== 'undefined' && !!navigator.geolocation && typeof navigator.geolocation.getCurrentPosition === 'function' && typeof navigator.geolocation.watchPosition === 'function' && typeof navigator.geolocation.clearWatch === 'function');
1630
1682
  const [coordinates, setCoordinates] = useState(initCoord);
1631
1683
  const [locatedAt, setLocatedAt] = useState(null);
1632
1684
  const [error, setError] = useState(null);
@@ -2337,13 +2389,14 @@ const useMousePressed = (target, options = defaultOptions$1)=>{
2337
2389
  const useMutationObserver = (callback, target, options = defaultOptions$1)=>{
2338
2390
  const callbackRef = useLatest(callback);
2339
2391
  const observerRef = useRef();
2392
+ const { key: targetKey, ref: targetRef } = useStableTarget(target);
2340
2393
  const stop = useCallback(()=>{
2341
2394
  if (observerRef.current) {
2342
2395
  observerRef.current.disconnect();
2343
2396
  }
2344
2397
  }, []);
2345
2398
  useDeepCompareEffect(()=>{
2346
- const element = getTargetElement(target);
2399
+ const element = getTargetElement(targetRef.current);
2347
2400
  if (!element) {
2348
2401
  return;
2349
2402
  }
@@ -2351,6 +2404,7 @@ const useMutationObserver = (callback, target, options = defaultOptions$1)=>{
2351
2404
  observerRef.current.observe(element, options);
2352
2405
  return stop;
2353
2406
  }, [
2407
+ targetKey,
2354
2408
  options
2355
2409
  ]);
2356
2410
  return stop;
@@ -3165,21 +3219,25 @@ const useSetState = (initialState)=>{
3165
3219
 
3166
3220
  function useSticky(targetElement, { axis = 'y', nav = 0 }, scrollElement) {
3167
3221
  const [isSticky, setSticky] = useState(false);
3222
+ const { key: targetKey, ref: targetRef } = useStableTarget(targetElement);
3223
+ const { key: scrollKey, ref: scrollRef } = useStableTarget(scrollElement);
3224
+ const axisRef = useLatest(axis);
3225
+ const navRef = useLatest(nav);
3168
3226
  const { run: scrollHandler } = useThrottleFn(()=>{
3169
- const element = getTargetElement(targetElement);
3227
+ const element = getTargetElement(targetRef.current);
3170
3228
  if (!element) {
3171
3229
  return;
3172
3230
  }
3173
3231
  const rect = element.getBoundingClientRect();
3174
- if (axis === 'y') {
3175
- setSticky((rect == null ? void 0 : rect.top) <= nav);
3232
+ if (axisRef.current === 'y') {
3233
+ setSticky((rect == null ? void 0 : rect.top) <= navRef.current);
3176
3234
  } else {
3177
- setSticky((rect == null ? void 0 : rect.left) <= nav);
3235
+ setSticky((rect == null ? void 0 : rect.left) <= navRef.current);
3178
3236
  }
3179
3237
  }, 50);
3180
3238
  useEffect(()=>{
3181
- const element = getTargetElement(targetElement);
3182
- const scrollParent = getTargetElement(scrollElement) || getScrollParent(axis, element);
3239
+ const element = getTargetElement(targetRef.current);
3240
+ const scrollParent = getTargetElement(scrollRef.current) || getScrollParent(axisRef.current, element);
3183
3241
  if (!element || !scrollParent) {
3184
3242
  return;
3185
3243
  }
@@ -3189,9 +3247,8 @@ function useSticky(targetElement, { axis = 'y', nav = 0 }, scrollElement) {
3189
3247
  scrollParent.removeEventListener('scroll', scrollHandler);
3190
3248
  };
3191
3249
  }, [
3192
- axis,
3193
- targetElement,
3194
- scrollElement,
3250
+ targetKey,
3251
+ scrollKey,
3195
3252
  scrollHandler
3196
3253
  ]);
3197
3254
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactuses/core",
3
- "version": "6.1.10",
3
+ "version": "6.1.12",
4
4
  "license": "Unlicense",
5
5
  "homepage": "https://www.reactuse.com/",
6
6
  "repository": {