react-hook-toolkit 4.0.0 → 5.0.0

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.
@@ -0,0 +1,408 @@
1
+ import { useState, useEffect, useCallback, useRef, useLayoutEffect } from 'react';
2
+ /**
3
+ * Manages a numeric counter with optional min/max bounds and a configurable step.
4
+ */
5
+ export function useCounter(initialValue = 0, options = {}) {
6
+ const { min = -Infinity, max = Infinity, step = 1 } = options;
7
+ const [count, setCount] = useState(() => Math.min(Math.max(initialValue, min), max));
8
+ const increment = useCallback(() => setCount(prev => Math.min(prev + step, max)), [max, step]);
9
+ const decrement = useCallback(() => setCount(prev => Math.max(prev - step, min)), [min, step]);
10
+ const reset = useCallback(() => setCount(Math.min(Math.max(initialValue, min), max)), [initialValue, min, max]);
11
+ const set = useCallback((value) => setCount(Math.min(Math.max(value, min), max)), [min, max]);
12
+ return { count, increment, decrement, reset, set };
13
+ }
14
+ /**
15
+ * Manages a Map data structure as React state with stable action methods.
16
+ */
17
+ export function useMap(initialEntries = []) {
18
+ const [map, setMap] = useState(() => new Map(initialEntries));
19
+ const set = useCallback((key, value) => {
20
+ setMap(prev => new Map(prev).set(key, value));
21
+ }, []);
22
+ const deleteKey = useCallback((key) => {
23
+ setMap(prev => {
24
+ const next = new Map(prev);
25
+ next.delete(key);
26
+ return next;
27
+ });
28
+ }, []);
29
+ const clear = useCallback(() => setMap(new Map()), []);
30
+ const get = useCallback((key) => map.get(key), [map]);
31
+ const has = useCallback((key) => map.has(key), [map]);
32
+ return { map, set, get, has, delete: deleteKey, clear, size: map.size };
33
+ }
34
+ /**
35
+ * Manages a Set data structure as React state with stable action methods.
36
+ */
37
+ export function useSet(initialValues = []) {
38
+ const [set, setSet] = useState(() => new Set(initialValues));
39
+ const add = useCallback((value) => {
40
+ setSet(prev => new Set(prev).add(value));
41
+ }, []);
42
+ const deleteValue = useCallback((value) => {
43
+ setSet(prev => {
44
+ const next = new Set(prev);
45
+ next.delete(value);
46
+ return next;
47
+ });
48
+ }, []);
49
+ const toggle = useCallback((value) => {
50
+ setSet(prev => {
51
+ const next = new Set(prev);
52
+ if (next.has(value))
53
+ next.delete(value);
54
+ else
55
+ next.add(value);
56
+ return next;
57
+ });
58
+ }, []);
59
+ const clear = useCallback(() => setSet(new Set()), []);
60
+ const has = useCallback((value) => set.has(value), [set]);
61
+ return { set, add, has, delete: deleteValue, toggle, clear, size: set.size };
62
+ }
63
+ /**
64
+ * Manages a FIFO queue as React state.
65
+ */
66
+ export function useQueue(initialItems = []) {
67
+ const [queue, setQueue] = useState(initialItems);
68
+ const enqueue = useCallback((item) => {
69
+ setQueue(prev => [...prev, item]);
70
+ }, []);
71
+ const dequeue = useCallback(() => {
72
+ let removed;
73
+ setQueue(prev => {
74
+ if (prev.length === 0)
75
+ return prev;
76
+ [removed] = prev;
77
+ return prev.slice(1);
78
+ });
79
+ return removed;
80
+ }, []);
81
+ const peek = useCallback(() => queue[0], [queue]);
82
+ const clear = useCallback(() => setQueue([]), []);
83
+ return { queue, enqueue, dequeue, peek, clear, size: queue.length, isEmpty: queue.length === 0 };
84
+ }
85
+ /**
86
+ * Manages a LIFO stack as React state.
87
+ */
88
+ export function useStack(initialItems = []) {
89
+ const [stack, setStack] = useState(initialItems);
90
+ const push = useCallback((item) => {
91
+ setStack(prev => [...prev, item]);
92
+ }, []);
93
+ const pop = useCallback(() => {
94
+ let removed;
95
+ setStack(prev => {
96
+ if (prev.length === 0)
97
+ return prev;
98
+ removed = prev[prev.length - 1];
99
+ return prev.slice(0, -1);
100
+ });
101
+ return removed;
102
+ }, []);
103
+ const peek = useCallback(() => stack[stack.length - 1], [stack]);
104
+ const clear = useCallback(() => setStack([]), []);
105
+ return { stack, push, pop, peek, clear, size: stack.length, isEmpty: stack.length === 0 };
106
+ }
107
+ /**
108
+ * Tracks focus state of a DOM element via a ref.
109
+ */
110
+ export function useFocus() {
111
+ const ref = useRef(null);
112
+ const [isFocused, setIsFocused] = useState(false);
113
+ useEffect(() => {
114
+ const el = ref.current;
115
+ if (!el)
116
+ return;
117
+ const onFocus = () => setIsFocused(true);
118
+ const onBlur = () => setIsFocused(false);
119
+ el.addEventListener('focus', onFocus);
120
+ el.addEventListener('blur', onBlur);
121
+ return () => {
122
+ el.removeEventListener('focus', onFocus);
123
+ el.removeEventListener('blur', onBlur);
124
+ };
125
+ });
126
+ const focus = useCallback(() => { var _a; return (_a = ref.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
127
+ const blur = useCallback(() => { var _a; return (_a = ref.current) === null || _a === void 0 ? void 0 : _a.blur(); }, []);
128
+ return { ref, isFocused, focus, blur };
129
+ }
130
+ // ─── useHover ─────────────────────────────────────────────────────────────────
131
+ /**
132
+ * Tracks whether a DOM element is being hovered via a ref.
133
+ * Returns a [ref, isHovered] tuple.
134
+ */
135
+ export function useHover() {
136
+ const ref = useRef(null);
137
+ const [isHovered, setIsHovered] = useState(false);
138
+ useEffect(() => {
139
+ const el = ref.current;
140
+ if (!el)
141
+ return;
142
+ const onEnter = () => setIsHovered(true);
143
+ const onLeave = () => setIsHovered(false);
144
+ el.addEventListener('mouseenter', onEnter);
145
+ el.addEventListener('mouseleave', onLeave);
146
+ return () => {
147
+ el.removeEventListener('mouseenter', onEnter);
148
+ el.removeEventListener('mouseleave', onLeave);
149
+ };
150
+ });
151
+ return [ref, isHovered];
152
+ }
153
+ /**
154
+ * Observes whether a ref'd element is intersecting the viewport (or a given root).
155
+ * When freezeOnceVisible is true the entry is frozen after first intersection.
156
+ */
157
+ export function useIntersectionObserver(elementRef, options = {}) {
158
+ const { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false } = options;
159
+ const [entry, setEntry] = useState();
160
+ const frozen = (entry === null || entry === void 0 ? void 0 : entry.isIntersecting) && freezeOnceVisible;
161
+ useEffect(() => {
162
+ const el = elementRef === null || elementRef === void 0 ? void 0 : elementRef.current;
163
+ if (frozen || !el || !('IntersectionObserver' in window))
164
+ return;
165
+ const observer = new IntersectionObserver(([newEntry]) => setEntry(newEntry), { threshold, root, rootMargin });
166
+ observer.observe(el);
167
+ return () => observer.disconnect();
168
+ }, [elementRef, threshold, root, rootMargin, frozen]);
169
+ return entry;
170
+ }
171
+ const defaultRect = { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 };
172
+ /**
173
+ * Measures the bounding rect of a DOM element, updating on resize.
174
+ */
175
+ export function useMeasure() {
176
+ const ref = useRef(null);
177
+ const [rect, setRect] = useState(defaultRect);
178
+ useLayoutEffect(() => {
179
+ const el = ref.current;
180
+ if (!el)
181
+ return;
182
+ const update = () => {
183
+ const r = el.getBoundingClientRect();
184
+ setRect({ x: r.x, y: r.y, width: r.width, height: r.height, top: r.top, right: r.right, bottom: r.bottom, left: r.left });
185
+ };
186
+ update();
187
+ const observer = new ResizeObserver(update);
188
+ observer.observe(el);
189
+ return () => observer.disconnect();
190
+ }, []);
191
+ return { ref, rect };
192
+ }
193
+ /**
194
+ * Returns detailed network connection information including online status,
195
+ * effective connection type, downlink speed, and RTT.
196
+ */
197
+ export function useNetwork() {
198
+ const getState = () => {
199
+ var _a, _b, _c, _d, _e;
200
+ const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
201
+ return {
202
+ isOnline: navigator.onLine,
203
+ effectiveType: (_a = conn === null || conn === void 0 ? void 0 : conn.effectiveType) !== null && _a !== void 0 ? _a : null,
204
+ downlink: (_b = conn === null || conn === void 0 ? void 0 : conn.downlink) !== null && _b !== void 0 ? _b : null,
205
+ rtt: (_c = conn === null || conn === void 0 ? void 0 : conn.rtt) !== null && _c !== void 0 ? _c : null,
206
+ saveData: (_d = conn === null || conn === void 0 ? void 0 : conn.saveData) !== null && _d !== void 0 ? _d : null,
207
+ type: (_e = conn === null || conn === void 0 ? void 0 : conn.type) !== null && _e !== void 0 ? _e : null,
208
+ };
209
+ };
210
+ const [state, setState] = useState(getState);
211
+ useEffect(() => {
212
+ const update = () => setState(getState());
213
+ window.addEventListener('online', update);
214
+ window.addEventListener('offline', update);
215
+ const conn = navigator.connection;
216
+ conn === null || conn === void 0 ? void 0 : conn.addEventListener('change', update);
217
+ return () => {
218
+ window.removeEventListener('online', update);
219
+ window.removeEventListener('offline', update);
220
+ conn === null || conn === void 0 ? void 0 : conn.removeEventListener('change', update);
221
+ };
222
+ }, []);
223
+ return state;
224
+ }
225
+ // ─── useHash ──────────────────────────────────────────────────────────────────
226
+ /**
227
+ * Reads and writes the URL hash (fragment) as state.
228
+ * Returns [hash, setHash] where hash does not include the leading '#'.
229
+ */
230
+ export function useHash() {
231
+ const [hash, setHashState] = useState(() => typeof window !== 'undefined' ? window.location.hash.slice(1) : '');
232
+ useEffect(() => {
233
+ const onHashChange = () => setHashState(window.location.hash.slice(1));
234
+ window.addEventListener('hashchange', onHashChange);
235
+ return () => window.removeEventListener('hashchange', onHashChange);
236
+ }, []);
237
+ const setHash = useCallback((newHash) => {
238
+ const normalized = newHash.startsWith('#') ? newHash : `#${newHash}`;
239
+ window.location.hash = normalized;
240
+ }, []);
241
+ return [hash, setHash];
242
+ }
243
+ // ─── useLatest ────────────────────────────────────────────────────────────────
244
+ /**
245
+ * Returns a ref that always holds the most recent value.
246
+ * Useful for reading the latest prop/state inside a callback without
247
+ * adding it to the dependency array.
248
+ */
249
+ export function useLatest(value) {
250
+ const ref = useRef(value);
251
+ ref.current = value;
252
+ return ref;
253
+ }
254
+ // ─── useEventCallback ─────────────────────────────────────────────────────────
255
+ /**
256
+ * Returns a stable callback reference that always delegates to the latest
257
+ * version of the provided function. Safe to pass to event listeners without
258
+ * causing unnecessary re-subscriptions.
259
+ */
260
+ export function useEventCallback(fn) {
261
+ const ref = useRef(fn);
262
+ useLayoutEffect(() => {
263
+ ref.current = fn;
264
+ });
265
+ return useCallback((...args) => ref.current(...args), []);
266
+ }
267
+ // ─── useSafeState ─────────────────────────────────────────────────────────────
268
+ /**
269
+ * A drop-in replacement for useState that suppresses state updates after the
270
+ * component has unmounted, preventing "Can't perform a React state update on an
271
+ * unmounted component" warnings.
272
+ */
273
+ export function useSafeState(initialState) {
274
+ const isMounted = useRef(false);
275
+ const [state, setState] = useState(initialState);
276
+ useEffect(() => {
277
+ isMounted.current = true;
278
+ return () => { isMounted.current = false; };
279
+ }, []);
280
+ const safeSetState = useCallback((value) => {
281
+ if (isMounted.current)
282
+ setState(value);
283
+ }, []);
284
+ return [state, safeSetState];
285
+ }
286
+ // ─── useRafState ──────────────────────────────────────────────────────────────
287
+ /**
288
+ * Identical to useState but batches updates via requestAnimationFrame,
289
+ * reducing the number of renders for rapidly changing values (e.g. mouse
290
+ * position, scroll events).
291
+ */
292
+ export function useRafState(initialState) {
293
+ const frameRef = useRef(0);
294
+ const [state, setState] = useState(initialState);
295
+ const setRafState = useCallback((value) => {
296
+ cancelAnimationFrame(frameRef.current);
297
+ frameRef.current = requestAnimationFrame(() => setState(value));
298
+ }, []);
299
+ useEffect(() => () => cancelAnimationFrame(frameRef.current), []);
300
+ return [state, setRafState];
301
+ }
302
+ /**
303
+ * Sets the document title reactively. Optionally restores the previous title
304
+ * on unmount.
305
+ */
306
+ export function useTitle(title, options = {}) {
307
+ const { restoreOnUnmount = false } = options;
308
+ const originalTitle = useRef(typeof document !== 'undefined' ? document.title : '');
309
+ useEffect(() => {
310
+ if (typeof document === 'undefined')
311
+ return;
312
+ document.title = title;
313
+ return () => {
314
+ if (restoreOnUnmount) {
315
+ document.title = originalTitle.current;
316
+ }
317
+ };
318
+ }, [title, restoreOnUnmount]);
319
+ }
320
+ // ─── useFavicon ───────────────────────────────────────────────────────────────
321
+ /**
322
+ * Dynamically updates the page favicon by href. Useful for notifications or
323
+ * theme changes (e.g. switching between light/dark favicon).
324
+ */
325
+ export function useFavicon(href) {
326
+ useEffect(() => {
327
+ if (typeof document === 'undefined' || !href)
328
+ return;
329
+ let link = document.querySelector('link[rel~="icon"]');
330
+ if (!link) {
331
+ link = document.createElement('link');
332
+ link.rel = 'icon';
333
+ document.head.appendChild(link);
334
+ }
335
+ link.href = href;
336
+ }, [href]);
337
+ }
338
+ // ─── useWhyDidYouUpdate ───────────────────────────────────────────────────────
339
+ /**
340
+ * Debug hook that logs which prop/state changes triggered the last render.
341
+ * Only active in development; no-ops in production.
342
+ * Pass a label and the component's props/state object.
343
+ */
344
+ export function useWhyDidYouUpdate(name, props) {
345
+ const previousProps = useRef({});
346
+ useEffect(() => {
347
+ if (process.env.NODE_ENV !== 'production') {
348
+ const allKeys = Object.keys(Object.assign(Object.assign({}, previousProps.current), props));
349
+ const changes = {};
350
+ allKeys.forEach(key => {
351
+ if (previousProps.current[key] !== props[key]) {
352
+ changes[key] = { from: previousProps.current[key], to: props[key] };
353
+ }
354
+ });
355
+ if (Object.keys(changes).length > 0) {
356
+ console.log(`[useWhyDidYouUpdate] ${name}`, changes);
357
+ }
358
+ }
359
+ previousProps.current = props;
360
+ });
361
+ }
362
+ /**
363
+ * Manages the Fullscreen API for a specific element (or the whole document when
364
+ * no ref target is provided). Tracks fullscreen state reactively.
365
+ */
366
+ export function useFullscreen() {
367
+ const ref = useRef(null);
368
+ const [isFullscreen, setIsFullscreen] = useState(false);
369
+ const isSupported = typeof document !== 'undefined' && 'fullscreenEnabled' in document;
370
+ useEffect(() => {
371
+ const onChange = () => setIsFullscreen(!!document.fullscreenElement);
372
+ document.addEventListener('fullscreenchange', onChange);
373
+ return () => document.removeEventListener('fullscreenchange', onChange);
374
+ }, []);
375
+ const enter = useCallback(async () => {
376
+ var _a, _b;
377
+ const el = (_a = ref.current) !== null && _a !== void 0 ? _a : document.documentElement;
378
+ if (!document.fullscreenElement) {
379
+ await ((_b = el.requestFullscreen) === null || _b === void 0 ? void 0 : _b.call(el));
380
+ }
381
+ }, []);
382
+ const exit = useCallback(async () => {
383
+ var _a;
384
+ if (document.fullscreenElement) {
385
+ await ((_a = document.exitFullscreen) === null || _a === void 0 ? void 0 : _a.call(document));
386
+ }
387
+ }, []);
388
+ const toggle = useCallback(async () => {
389
+ var _a, _b, _c, _d;
390
+ if (document.fullscreenElement)
391
+ await ((_a = document.exitFullscreen) === null || _a === void 0 ? void 0 : _a.call(document));
392
+ else
393
+ await ((_d = (_c = ((_b = ref.current) !== null && _b !== void 0 ? _b : document.documentElement)).requestFullscreen) === null || _d === void 0 ? void 0 : _d.call(_c));
394
+ }, []);
395
+ return { ref, isFullscreen, enter, exit, toggle, isSupported };
396
+ }
397
+ // ─── useLogger ────────────────────────────────────────────────────────────────
398
+ /**
399
+ * Logs the component name and any values passed on every render.
400
+ * Only logs in development mode.
401
+ */
402
+ export function useLogger(name, ...values) {
403
+ useEffect(() => {
404
+ if (process.env.NODE_ENV !== 'production') {
405
+ console.log(`[${name}]`, ...values);
406
+ }
407
+ });
408
+ }