msr-hooks 1.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.
Files changed (113) hide show
  1. package/README.md +1252 -0
  2. package/package.json +50 -0
  3. package/src/hooks/index.js +53 -0
  4. package/src/hooks/useAsyncEffect/index.js +1 -0
  5. package/src/hooks/useAsyncEffect/useAsyncEffect.d.ts +6 -0
  6. package/src/hooks/useAsyncEffect/useAsyncEffect.js +31 -0
  7. package/src/hooks/useChangeIconColor/index.js +1 -0
  8. package/src/hooks/useChangeIconColor/useChangeIconColor.d.ts +1 -0
  9. package/src/hooks/useChangeIconColor/useChangeIconColor.js +39 -0
  10. package/src/hooks/useClickOutsideObject/index.js +1 -0
  11. package/src/hooks/useClickOutsideObject/useClickOutsideObject.d.ts +8 -0
  12. package/src/hooks/useClickOutsideObject/useClickOutsideObject.js +28 -0
  13. package/src/hooks/useClipboard/index.js +1 -0
  14. package/src/hooks/useClipboard/useClipboard.d.ts +4 -0
  15. package/src/hooks/useClipboard/useClipboard.js +27 -0
  16. package/src/hooks/useDebounce/index.js +1 -0
  17. package/src/hooks/useDebounce/useDebounce.d.ts +1 -0
  18. package/src/hooks/useDebounce/useDebounce.js +18 -0
  19. package/src/hooks/useDeepCompareEffect/index.js +1 -0
  20. package/src/hooks/useDeepCompareEffect/useDeepCompareEffect.d.ts +3 -0
  21. package/src/hooks/useDeepCompareEffect/useDeepCompareEffect.js +40 -0
  22. package/src/hooks/useDocumentVisibility/index.js +1 -0
  23. package/src/hooks/useDocumentVisibility/useDocumentVisibility.d.ts +1 -0
  24. package/src/hooks/useDocumentVisibility/useDocumentVisibility.js +19 -0
  25. package/src/hooks/useEffectAfterMount/index.js +1 -0
  26. package/src/hooks/useEffectAfterMount/useEffectAfterMount.d.ts +4 -0
  27. package/src/hooks/useEffectAfterMount/useEffectAfterMount.js +20 -0
  28. package/src/hooks/useElementScrollProgress/index.js +1 -0
  29. package/src/hooks/useElementScrollProgress/useElementScrollProgress.d.ts +5 -0
  30. package/src/hooks/useElementScrollProgress/useElementScrollProgress.js +37 -0
  31. package/src/hooks/useEscapeKey/index.js +1 -0
  32. package/src/hooks/useEscapeKey/useEscapeKey.d.ts +1 -0
  33. package/src/hooks/useEscapeKey/useEscapeKey.js +22 -0
  34. package/src/hooks/useEventListener/index.js +1 -0
  35. package/src/hooks/useEventListener/useEventListener.d.ts +8 -0
  36. package/src/hooks/useEventListener/useEventListener.js +25 -0
  37. package/src/hooks/useFetch/index.js +1 -0
  38. package/src/hooks/useFetch/useFetch.d.ts +11 -0
  39. package/src/hooks/useFetch/useFetch.js +35 -0
  40. package/src/hooks/useHoverIntent/index.js +1 -0
  41. package/src/hooks/useHoverIntent/useHoverIntent.d.ts +6 -0
  42. package/src/hooks/useHoverIntent/useHoverIntent.js +81 -0
  43. package/src/hooks/useIntersectionObserver/index.js +1 -0
  44. package/src/hooks/useIntersectionObserver/useIntersectionObserver.d.ts +5 -0
  45. package/src/hooks/useIntersectionObserver/useIntersectionObserver.js +25 -0
  46. package/src/hooks/useInterval/index.js +1 -0
  47. package/src/hooks/useInterval/useInterval.d.ts +4 -0
  48. package/src/hooks/useInterval/useInterval.js +23 -0
  49. package/src/hooks/useIsomorphicLayoutEffect/index.js +1 -0
  50. package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.d.ts +3 -0
  51. package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.js +3 -0
  52. package/src/hooks/useKeyPressSequence/index.js +1 -0
  53. package/src/hooks/useKeyPressSequence/useKeyPressSequence.d.ts +5 -0
  54. package/src/hooks/useKeyPressSequence/useKeyPressSequence.js +52 -0
  55. package/src/hooks/useKeyboardNavigation/index.js +1 -0
  56. package/src/hooks/useKeyboardNavigation/useKeyboardNavigation.d.ts +10 -0
  57. package/src/hooks/useKeyboardNavigation/useKeyboardNavigation.js +50 -0
  58. package/src/hooks/useLocalStorage/index.js +1 -0
  59. package/src/hooks/useLocalStorage/useLocalStorage.d.ts +4 -0
  60. package/src/hooks/useLocalStorage/useLocalStorage.js +44 -0
  61. package/src/hooks/useLockBodyScroll/index.js +1 -0
  62. package/src/hooks/useLockBodyScroll/useLockBodyScroll.d.ts +1 -0
  63. package/src/hooks/useLockBodyScroll/useLockBodyScroll.js +25 -0
  64. package/src/hooks/useMediaQuery/index.js +1 -0
  65. package/src/hooks/useMediaQuery/useMediaQuery.d.ts +1 -0
  66. package/src/hooks/useMediaQuery/useMediaQuery.js +24 -0
  67. package/src/hooks/useNetworkStatus/index.js +1 -0
  68. package/src/hooks/useNetworkStatus/useNetworkStatus.d.ts +7 -0
  69. package/src/hooks/useNetworkStatus/useNetworkStatus.js +47 -0
  70. package/src/hooks/usePageLeave/index.js +1 -0
  71. package/src/hooks/usePageLeave/usePageLeave.d.ts +1 -0
  72. package/src/hooks/usePageLeave/usePageLeave.js +27 -0
  73. package/src/hooks/useParentWidth/index.js +1 -0
  74. package/src/hooks/useParentWidth/useParentWidth.d.ts +8 -0
  75. package/src/hooks/useParentWidth/useParentWidth.js +30 -0
  76. package/src/hooks/usePortal/index.js +1 -0
  77. package/src/hooks/usePortal/usePortal.d.ts +1 -0
  78. package/src/hooks/usePortal/usePortal.js +33 -0
  79. package/src/hooks/usePrefersReducedMotion/index.js +1 -0
  80. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.d.ts +1 -0
  81. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.js +24 -0
  82. package/src/hooks/usePreventZoom/index.js +1 -0
  83. package/src/hooks/usePreventZoom/usePreventZoom.d.ts +4 -0
  84. package/src/hooks/usePreventZoom/usePreventZoom.js +39 -0
  85. package/src/hooks/usePrevious/index.js +1 -0
  86. package/src/hooks/usePrevious/usePrevious.d.ts +1 -0
  87. package/src/hooks/usePrevious/usePrevious.js +16 -0
  88. package/src/hooks/useResize/index.js +1 -0
  89. package/src/hooks/useResize/useResize.d.ts +16 -0
  90. package/src/hooks/useResize/useResize.js +32 -0
  91. package/src/hooks/useSpringValue/index.js +1 -0
  92. package/src/hooks/useSpringValue/useSpringValue.d.ts +4 -0
  93. package/src/hooks/useSpringValue/useSpringValue.js +66 -0
  94. package/src/hooks/useStateHistory/index.js +1 -0
  95. package/src/hooks/useStateHistory/useStateHistory.d.ts +17 -0
  96. package/src/hooks/useStateHistory/useStateHistory.js +70 -0
  97. package/src/hooks/useThrottle/index.js +1 -0
  98. package/src/hooks/useThrottle/useThrottle.d.ts +1 -0
  99. package/src/hooks/useThrottle/useThrottle.js +25 -0
  100. package/src/hooks/useTimeout/index.js +1 -0
  101. package/src/hooks/useTimeout/useTimeout.d.ts +4 -0
  102. package/src/hooks/useTimeout/useTimeout.js +22 -0
  103. package/src/hooks/useToggle/index.js +1 -0
  104. package/src/hooks/useToggle/useToggle.d.ts +3 -0
  105. package/src/hooks/useToggle/useToggle.js +16 -0
  106. package/src/hooks/useUndoRedo/index.js +1 -0
  107. package/src/hooks/useUndoRedo/useUndoRedo.d.ts +18 -0
  108. package/src/hooks/useUndoRedo/useUndoRedo.js +49 -0
  109. package/src/hooks/useWindowSize/index.js +1 -0
  110. package/src/hooks/useWindowSize/useWindowSize.d.ts +1 -0
  111. package/src/hooks/useWindowSize/useWindowSize.js +29 -0
  112. package/src/index.d.ts +1 -0
  113. package/src/index.js +1 -0
package/README.md ADDED
@@ -0,0 +1,1252 @@
1
+ <div align="center">
2
+
3
+ # 🪝 msr-hooks
4
+
5
+ ### A comprehensive collection of production-ready React hooks
6
+
7
+ [![npm version](https://img.shields.io/npm/v/msr-hooks.svg)](https://www.npmjs.com/package/msr-hooks)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
10
+
11
+ **40+ hooks** • **TypeScript & JavaScript** • **SSR-Safe** • **Tree-Shakeable** • **Zero Dependencies**
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ ## 📦 Installation
18
+
19
+ ```bash
20
+ # npm
21
+ npm install msr-hooks
22
+
23
+ # yarn
24
+ yarn add msr-hooks
25
+
26
+ # pnpm
27
+ pnpm add msr-hooks
28
+ ```
29
+
30
+ ---
31
+
32
+ ## ✨ Features
33
+
34
+ - 🎯 **40+ Production-Ready Hooks** - Cover all common use cases and advanced patterns
35
+ - 🔷 **Full TypeScript Support** - Complete type definitions included
36
+ - 🌐 **SSR-Safe** - Proper guards for Next.js, Gatsby, and other SSR frameworks
37
+ - 🌲 **Tree-Shakeable** - Import only what you need
38
+ - 📦 **Zero Dependencies** - Only React as peer dependency
39
+ - 📚 **Well-Documented** - JSDoc comments for IntelliSense
40
+ - ⚡ **Lightweight** - Minimal bundle impact
41
+ - ⏱️ **Time-Travel State** - Full timeline navigation with useStateHistory
42
+
43
+ ---
44
+
45
+ ## 📚 Available Hooks
46
+
47
+ ### 🔧 Core Utilities (6 hooks)
48
+ | Hook | Description |
49
+ |------|-------------|
50
+ | `useEffectAfterMount` | Run effect only after component mounts |
51
+ | `useWindowSize` | Track window dimensions |
52
+ | `useDebounce` | Debounce values |
53
+ | `usePrevious` | Access previous prop/state value |
54
+ | `useToggle` | Simple boolean toggle state |
55
+ | `useLocalStorage` | Persist state to localStorage |
56
+
57
+ ### 🎨 UI & Interaction (7 hooks)
58
+ | Hook | Description |
59
+ |------|-------------|
60
+ | `usePreventZoom` | Prevent pinch-zoom on mobile |
61
+ | `useChangeIconColor` | Dynamically change SVG icon colors |
62
+ | `useClickOutsideObject` | Detect clicks outside an element |
63
+ | `useKeyboardNavigation` | Navigate with arrow keys |
64
+ | `useEscapeKey` | Handle Escape key presses |
65
+ | `useParentWidth` | Track parent element width |
66
+ | `useResize` | Monitor element resize |
67
+
68
+ ### 🛠️ Utility Hooks (7 hooks)
69
+ | Hook | Description |
70
+ |------|-------------|
71
+ | `useMediaQuery` | Reactive media query matching |
72
+ | `useClipboard` | Copy/paste clipboard operations |
73
+ | `useInterval` | Controlled interval with cleanup |
74
+ | `useTimeout` | Controlled timeout with cleanup |
75
+ | `useThrottle` | Throttle function execution |
76
+ | `useIntersectionObserver` | Detect element visibility |
77
+ | `useFetch` | Data fetching with caching |
78
+
79
+ ### 📊 State Management & History (2 hooks)
80
+ | Hook | Description |
81
+ |------|-------------|
82
+ | `useUndoRedo` | Simple undo/redo (last 50 states) |
83
+ | `useStateHistory` | Full timeline with jump-to-index navigation |
84
+
85
+ ### 🎬 Scroll & Animation (2 hooks)
86
+ | Hook | Description |
87
+ |------|-------------|
88
+ | `useElementScrollProgress` | Track scroll progress (0-1) |
89
+ | `useSpringValue` | Spring physics animation |
90
+
91
+ ### 🌐 Network & Browser (4 hooks)
92
+ | Hook | Description |
93
+ |------|-------------|
94
+ | `useNetworkStatus` | Online/offline + connection quality |
95
+ | `useDocumentVisibility` | Tab focus/blur detection |
96
+ | `usePageLeave` | Trigger on page/tab close |
97
+ | `usePrefersReducedMotion` | Detect motion preference |
98
+
99
+ ### 🏗️ DOM & Layout (2 hooks)
100
+ | Hook | Description |
101
+ |------|-------------|
102
+ | `useLockBodyScroll` | Lock/unlock body scrolling |
103
+ | `usePortal` | Portal element management |
104
+
105
+ ### ⌨️ Events & Interaction (3 hooks)
106
+ | Hook | Description |
107
+ |------|-------------|
108
+ | `useKeyPressSequence` | Detect key sequences (like Konami codes) |
109
+ | `useHoverIntent` | Smart hover detection with delay |
110
+ | `useEventListener` | Generic event listener with cleanup |
111
+
112
+ ### 🚀 Advanced Effects (3 hooks)
113
+ | Hook | Description |
114
+ |------|-------------|
115
+ | `useAsyncEffect` | Async effect with AbortSignal |
116
+ | `useDeepCompareEffect` | Effect with deep dependency comparison |
117
+ | `useIsomorphicLayoutEffect` | SSR-safe useLayoutEffect |
118
+
119
+ ---
120
+
121
+ ## 📖 Quick Examples
122
+
123
+ ### useStateHistory - Time-Travel State
124
+
125
+ <details open>
126
+ <summary>Full Timeline Navigation</summary>
127
+
128
+ ```javascript
129
+ import { useStateHistory } from 'msr-hooks';
130
+
131
+ function Editor() {
132
+ const {
133
+ state,
134
+ set,
135
+ history,
136
+ pointer,
137
+ jump,
138
+ canUndo,
139
+ canRedo,
140
+ undo,
141
+ redo,
142
+ clearHistory,
143
+ } = useStateHistory({ text: '' });
144
+
145
+ return (
146
+ <div>
147
+ <textarea
148
+ value={state.text}
149
+ onChange={(e) => set({ text: e.target.value })}
150
+ />
151
+
152
+ <div>
153
+ <button onClick={undo} disabled={!canUndo}>↶ Undo</button>
154
+ <button onClick={redo} disabled={!canRedo}>↷ Redo</button>
155
+ <button onClick={clearHistory}>Clear</button>
156
+ <span>Position: {pointer + 1} / {history.length}</span>
157
+ </div>
158
+
159
+ <div>
160
+ <h4>Timeline:</h4>
161
+ {history.map((entry, idx) => (
162
+ <button
163
+ key={idx}
164
+ onClick={() => jump(idx)}
165
+ style={{ fontWeight: idx === pointer ? 'bold' : 'normal' }}
166
+ >
167
+ {idx}: {entry.text || '(empty)'}
168
+ </button>
169
+ ))}
170
+ </div>
171
+ </div>
172
+ );
173
+ }
174
+ ```
175
+
176
+ </details>
177
+
178
+ ### useMediaQuery - Responsive Design
179
+
180
+ <details>
181
+ <summary>JavaScript</summary>
182
+
183
+ ```javascript
184
+ import { useMediaQuery } from 'msr-hooks';
185
+
186
+ function ResponsiveNav() {
187
+ const isMobile = useMediaQuery('(max-width: 768px)');
188
+ const isDesktop = useMediaQuery('(min-width: 1025px)');
189
+
190
+ return (
191
+ <nav>
192
+ {isMobile && <MobileMenu />}
193
+ {isDesktop && <DesktopMenu />}
194
+ </nav>
195
+ );
196
+ }
197
+ ```
198
+
199
+ </details>
200
+
201
+ ### useFetch - Data Fetching
202
+
203
+ <details>
204
+ <summary>JavaScript</summary>
205
+
206
+ ```javascript
207
+ import { useFetch, useDebounce } from 'msr-hooks';
208
+ import { useState } from 'react';
209
+
210
+ function UsersList() {
211
+ const [search, setSearch] = useState('');
212
+ const debouncedSearch = useDebounce(search, 300);
213
+
214
+ const { data, loading, error, refetch } = useFetch(
215
+ 'https://api.example.com/users'
216
+ );
217
+
218
+ if (loading) return <Spinner />;
219
+ if (error) return <div>Error: {error}</div>;
220
+
221
+ return (
222
+ <div>
223
+ <input
224
+ value={search}
225
+ onChange={(e) => setSearch(e.target.value)}
226
+ />
227
+ <ul>
228
+ {data?.map(user => (
229
+ <li key={user.id}>{user.name}</li>
230
+ ))}
231
+ </ul>
232
+ </div>
233
+ );
234
+ }
235
+ ```
236
+
237
+ </details>
238
+
239
+ ### useLocalStorage - Persist State
240
+
241
+ <details>
242
+ <summary>JavaScript</summary>
243
+
244
+ ```javascript
245
+ import { useLocalStorage } from 'msr-hooks';
246
+
247
+ function Settings() {
248
+ const [theme, setTheme] = useLocalStorage('theme', 'light');
249
+
250
+ return (
251
+ <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
252
+ Current: {theme}
253
+ </button>
254
+ );
255
+ }
256
+ ```
257
+
258
+ </details>
259
+
260
+ ---
261
+
262
+ ## 🚀 Advanced Examples
263
+
264
+ ### useHoverIntent - Smart Hover Detection
265
+
266
+ ```javascript
267
+ import { useRef } from 'react';
268
+ import { useHoverIntent } from 'msr-hooks';
269
+
270
+ function Tooltip() {
271
+ const ref = useRef(null);
272
+ const hovered = useHoverIntent(ref, {
273
+ delay: 120,
274
+ leaveDelay: 80,
275
+ sensitivity: 8,
276
+ });
277
+
278
+ return (
279
+ <div ref={ref}>
280
+ Hover here
281
+ {hovered && <TooltipContent />}
282
+ </div>
283
+ );
284
+ }
285
+ ```
286
+
287
+ ### useAsyncEffect - Async Operations with Cleanup
288
+
289
+ ```javascript
290
+ import { useAsyncEffect } from 'msr-hooks';
291
+
292
+ function DataLoader() {
293
+ const [data, setData] = useState(null);
294
+
295
+ useAsyncEffect(async (signal) => {
296
+ const response = await fetch('/api/data', { signal });
297
+ if (!signal.aborted) {
298
+ setData(await response.json());
299
+ }
300
+ }, []);
301
+
302
+ return <div>{data?.title}</div>;
303
+ }
304
+ ```
305
+
306
+ ### useSpringValue - Physics-Based Animation
307
+
308
+ ```javascript
309
+ import { useSpringValue } from 'msr-hooks';
310
+
311
+ function Counter({ target }) {
312
+ const animated = useSpringValue(target, {
313
+ stiffness: 170,
314
+ damping: 26,
315
+ mass: 1,
316
+ });
317
+
318
+ return <div>{Math.round(animated)}</div>;
319
+ }
320
+ ```
321
+
322
+ ---
323
+
324
+ ## 🔗 API Reference
325
+
326
+ Visit the [GitHub repository](https://github.com/Minka1902/msr-hooks) for detailed API documentation for each hook.
327
+
328
+ ---
329
+
330
+ ## 📄 License
331
+
332
+ MIT © MSR
333
+
334
+ ---
335
+
336
+ ## 🤝 Contributing
337
+
338
+ Contributions are welcome! Please feel free to submit a Pull Request.
339
+ <input
340
+ type="text"
341
+ placeholder="Search posts..."
342
+ value={searchTerm}
343
+ onChange={(e) => setSearchTerm(e.target.value)}
344
+ />
345
+ <p>Found {filteredPosts.length} posts</p>
346
+ {filteredPosts.map(post => (
347
+ <Post key={post.id} {...post} />
348
+ ))}
349
+ <button onClick={refetch}>Refresh Posts</button>
350
+ </div>
351
+ );
352
+ }
353
+ ```
354
+
355
+ </details>
356
+
357
+ <details>
358
+ <summary>TypeScript</summary>
359
+
360
+ ```typescript
361
+ import { useFetch, type UseFetchReturn } from 'msr-hooks';
362
+
363
+ interface Post {
364
+ id: number;
365
+ title: string;
366
+ body: string;
367
+ }
368
+
369
+ const PostsList: React.FC = () => {
370
+ const { data, loading, error, refetch }:
371
+ UseFetchReturn<Post[]> = useFetch<Post[]>(
372
+ 'https://jsonplaceholder.typicode.com/posts'
373
+ );
374
+
375
+ if (loading) return <Spinner />;
376
+ if (error) return <ErrorMessage error={error} />;
377
+
378
+ return (
379
+ <div>
380
+ {data?.map((post: Post) => (
381
+ <article key={post.id}>
382
+ <h2>{post.title}</h2>
383
+ <p>{post.body}</p>
384
+ </article>
385
+ ))}
386
+ <button onClick={refetch}>Refresh</button>
387
+ </div>
388
+ );
389
+ };
390
+ ```
391
+
392
+ </details>
393
+
394
+ ### useClipboard - Copy to Clipboard
395
+
396
+ <details open>
397
+ <summary>JavaScript</summary>
398
+
399
+ ```javascript
400
+ import { useClipboard, useTimeout } from 'msr-hooks';
401
+ import { useState } from 'react';
402
+
403
+ function CodeBlock({ code, language = 'javascript' }) {
404
+ const [copy, isCopied] = useClipboard();
405
+ const [showNotification, setShowNotification] = useState(false);
406
+
407
+ const handleCopy = async () => {
408
+ await copy(code);
409
+ setShowNotification(true);
410
+ };
411
+
412
+ // Hide notification after 2 seconds
413
+ useTimeout(() => {
414
+ if (showNotification) setShowNotification(false);
415
+ }, showNotification ? 2000 : null);
416
+
417
+ return (
418
+ <div className="code-block">
419
+ <div className="code-header">
420
+ <span className="language">{language}</span>
421
+ <button
422
+ onClick={handleCopy}
423
+ className={isCopied ? 'copied' : ''}
424
+ >
425
+ {isCopied ? '✓ Copied!' : '📋 Copy'}
426
+ </button>
427
+ </div>
428
+ <pre><code>{code}</code></pre>
429
+ {showNotification && (
430
+ <div className="notification">
431
+ Copied to clipboard!
432
+ </div>
433
+ )}
434
+ </div>
435
+ );
436
+ }
437
+ ```
438
+
439
+ </details>
440
+
441
+ <details>
442
+ <summary>TypeScript</summary>
443
+
444
+ ```typescript
445
+ import { useClipboard } from 'msr-hooks';
446
+
447
+ interface CodeBlockProps {
448
+ code: string;
449
+ }
450
+
451
+ const CodeBlock: React.FC<CodeBlockProps> = ({ code }) => {
452
+ const [copy, isCopied]:
453
+ [(text: string) => Promise<void>, boolean] =
454
+ useClipboard();
455
+
456
+ return (
457
+ <div className="code-block">
458
+ <pre>{code}</pre>
459
+ <button onClick={() => copy(code)}>
460
+ {isCopied ? '✓ Copied!' : 'Copy'}
461
+ </button>
462
+ </div>
463
+ );
464
+ };
465
+ ```
466
+
467
+ </details>
468
+
469
+ ### useInterval & useTimeout - Timers
470
+
471
+ <details open>
472
+ <summary>JavaScript</summary>
473
+
474
+ ```javascript
475
+ import {
476
+ useInterval,
477
+ useTimeout,
478
+ useToggle,
479
+ useLocalStorage
480
+ } from 'msr-hooks';
481
+ import { useState } from 'react';
482
+
483
+ function Timer() {
484
+ const [count, setCount] = useState(0);
485
+ const [isPaused, togglePause] = useToggle(false);
486
+ const [speed, setSpeed] = useState(1000);
487
+ const [showWarning, setShowWarning] = useState(false);
488
+ const [bestTime] = useLocalStorage('bestTime', 0);
489
+
490
+ // Interval with dynamic speed
491
+ useInterval(() => {
492
+ setCount(c => c + 1);
493
+ }, isPaused ? null : speed);
494
+
495
+ // Show warning at 30 seconds
496
+ useTimeout(() => {
497
+ if (count >= 30 && !showWarning) {
498
+ setShowWarning(true);
499
+ }
500
+ }, count >= 30 && !showWarning ? 100 : null);
501
+
502
+ const reset = () => {
503
+ setCount(0);
504
+ setShowWarning(false);
505
+ };
506
+
507
+ return (
508
+ <div className="timer">
509
+ <h2>Count: {count}s</h2>
510
+ {bestTime > 0 && <p>Best: {bestTime}s</p>}
511
+ {showWarning && (
512
+ <div className="warning">⚠️ 30 seconds!</div>
513
+ )}
514
+ <div className="controls">
515
+ <button onClick={togglePause}>
516
+ {isPaused ? '▶️ Resume' : '⏸️ Pause'}
517
+ </button>
518
+ <button onClick={reset}>🔄 Reset</button>
519
+ <select
520
+ value={speed}
521
+ onChange={(e) => setSpeed(Number(e.target.value))}
522
+ >
523
+ <option value={500}>Fast (0.5s)</option>
524
+ <option value={1000}>Normal (1s)</option>
525
+ <option value={2000}>Slow (2s)</option>
526
+ </select>
527
+ </div>
528
+ </div>
529
+ );
530
+ }
531
+ ```
532
+
533
+ </details>
534
+
535
+ <details>
536
+ <summary>TypeScript</summary>
537
+
538
+ ```typescript
539
+ import { useInterval, useTimeout } from 'msr-hooks';
540
+
541
+ const Timer: React.FC = () => {
542
+ const [count, setCount] = useState<number>(0);
543
+ const [isPaused, setIsPaused] = useState<boolean>(false);
544
+
545
+ useInterval(() => {
546
+ setCount((c: number) => c + 1);
547
+ }, isPaused ? null : 1000);
548
+
549
+ useTimeout(() => {
550
+ alert('5 seconds passed!');
551
+ }, 5000);
552
+
553
+ return (
554
+ <div>
555
+ <p>Count: {count}</p>
556
+ <button onClick={() => setIsPaused(!isPaused)}>
557
+ {isPaused ? 'Resume' : 'Pause'}
558
+ </button>
559
+ </div>
560
+ );
561
+ };
562
+ ```
563
+
564
+ </details>
565
+
566
+ ### useIntersectionObserver - Lazy Loading
567
+
568
+ <details open>
569
+ <summary>JavaScript</summary>
570
+
571
+ ```javascript
572
+ import {
573
+ useIntersectionObserver,
574
+ useToggle
575
+ } from 'msr-hooks';
576
+ import { useState, useEffect } from 'react';
577
+
578
+ function LazyImage({ src, alt, lowQualitySrc }) {
579
+ const [ref, isVisible] = useIntersectionObserver({
580
+ threshold: 0.1,
581
+ rootMargin: '50px'
582
+ });
583
+ const [imageLoaded, setImageLoaded] = useState(false);
584
+ const [hasError, setHasError] = useState(false);
585
+ const [showDetails, toggleDetails] = useToggle(false);
586
+
587
+ useEffect(() => {
588
+ if (isVisible && !imageLoaded) {
589
+ // Preload the image
590
+ const img = new Image();
591
+ img.src = src;
592
+ img.onload = () => setImageLoaded(true);
593
+ img.onerror = () => setHasError(true);
594
+ }
595
+ }, [isVisible, src, imageLoaded]);
596
+
597
+ return (
598
+ <div
599
+ ref={ref}
600
+ className="image-container"
601
+ onClick={toggleDetails}
602
+ >
603
+ {!isVisible && (
604
+ <div className="placeholder">
605
+ <div className="skeleton" />
606
+ </div>
607
+ )}
608
+ {isVisible && !imageLoaded && !hasError && (
609
+ <img
610
+ src={lowQualitySrc}
611
+ alt={alt}
612
+ className="blur"
613
+ />
614
+ )}
615
+ {imageLoaded && (
616
+ <img
617
+ src={src}
618
+ alt={alt}
619
+ className="fade-in"
620
+ />
621
+ )}
622
+ {hasError && (
623
+ <div className="error">Failed to load image</div>
624
+ )}
625
+ {showDetails && imageLoaded && (
626
+ <div className="overlay">
627
+ <p>{alt}</p>
628
+ </div>
629
+ )}
630
+ </div>
631
+ );
632
+ }
633
+ ```
634
+
635
+ </details>
636
+
637
+ <details>
638
+ <summary>TypeScript</summary>
639
+
640
+ ```typescript
641
+ import { useIntersectionObserver } from 'msr-hooks';
642
+
643
+ interface LazyImageProps {
644
+ src: string;
645
+ alt: string;
646
+ }
647
+
648
+ const LazyImage: React.FC<LazyImageProps> = ({ src, alt }) => {
649
+ const [ref, isVisible]:
650
+ [React.RefObject<HTMLElement>, boolean] =
651
+ useIntersectionObserver({ threshold: 0.1 });
652
+
653
+ return (
654
+ <div ref={ref as React.RefObject<HTMLDivElement>}>
655
+ {isVisible && <img src={src} alt={alt} />}
656
+ </div>
657
+ );
658
+ };
659
+ ```
660
+
661
+ </details>
662
+
663
+ ### useLocalStorage - Persistent State
664
+
665
+ <details open>
666
+ <summary>JavaScript</summary>
667
+
668
+ ```javascript
669
+ import {
670
+ useLocalStorage,
671
+ useMediaQuery,
672
+ useToggle
673
+ } from 'msr-hooks';
674
+ import { useEffect } from 'react';
675
+
676
+ function Settings() {
677
+ const systemPrefersDark = useMediaQuery(
678
+ '(prefers-color-scheme: dark)'
679
+ );
680
+
681
+ const [settings, setSettings] = useLocalStorage(
682
+ 'app-settings',
683
+ {
684
+ theme: 'auto',
685
+ notifications: true,
686
+ language: 'en',
687
+ fontSize: 'medium',
688
+ soundEnabled: true
689
+ }
690
+ );
691
+
692
+ const [showAdvanced, toggleAdvanced] = useToggle(false);
693
+
694
+ // Auto theme based on system preference
695
+ const effectiveTheme = settings.theme === 'auto'
696
+ ? (systemPrefersDark ? 'dark' : 'light')
697
+ : settings.theme;
698
+
699
+ useEffect(() => {
700
+ document.body.className = effectiveTheme;
701
+ }, [effectiveTheme]);
702
+
703
+ const updateSetting = (key, value) => {
704
+ setSettings({ ...settings, [key]: value });
705
+ };
706
+
707
+ const resetToDefaults = () => {
708
+ setSettings({
709
+ theme: 'auto',
710
+ notifications: true,
711
+ language: 'en',
712
+ fontSize: 'medium',
713
+ soundEnabled: true
714
+ });
715
+ };
716
+
717
+ return (
718
+ <div className="settings">
719
+ <h2>Settings</h2>
720
+
721
+ <div className="setting-group">
722
+ <label>Theme</label>
723
+ <select
724
+ value={settings.theme}
725
+ onChange={(e) => updateSetting('theme', e.target.value)}
726
+ >
727
+ <option value="auto">Auto</option>
728
+ <option value="light">Light</option>
729
+ <option value="dark">Dark</option>
730
+ </select>
731
+ <small>Current: {effectiveTheme}</small>
732
+ </div>
733
+
734
+ <div className="setting-group">
735
+ <label>
736
+ <input
737
+ type="checkbox"
738
+ checked={settings.notifications}
739
+ onChange={(e) =>
740
+ updateSetting('notifications', e.target.checked)
741
+ }
742
+ />
743
+ Enable Notifications
744
+ </label>
745
+ </div>
746
+
747
+ <button onClick={toggleAdvanced}>
748
+ {showAdvanced ? 'Hide' : 'Show'} Advanced
749
+ </button>
750
+
751
+ {showAdvanced && (
752
+ <div className="advanced">
753
+ <div className="setting-group">
754
+ <label>Font Size</label>
755
+ <select
756
+ value={settings.fontSize}
757
+ onChange={(e) =>
758
+ updateSetting('fontSize', e.target.value)
759
+ }
760
+ >
761
+ <option value="small">Small</option>
762
+ <option value="medium">Medium</option>
763
+ <option value="large">Large</option>
764
+ </select>
765
+ </div>
766
+
767
+ <div className="setting-group">
768
+ <label>
769
+ <input
770
+ type="checkbox"
771
+ checked={settings.soundEnabled}
772
+ onChange={(e) =>
773
+ updateSetting('soundEnabled', e.target.checked)
774
+ }
775
+ />
776
+ Sound Effects
777
+ </label>
778
+ </div>
779
+ </div>
780
+ )}
781
+
782
+ <button onClick={resetToDefaults}>
783
+ Reset to Defaults
784
+ </button>
785
+ </div>
786
+ );
787
+ }
788
+ ```
789
+
790
+ </details>
791
+
792
+ <details>
793
+ <summary>TypeScript</summary>
794
+
795
+ ```typescript
796
+ import { useLocalStorage } from 'msr-hooks';
797
+
798
+ interface AppSettings {
799
+ theme: 'light' | 'dark';
800
+ notifications: boolean;
801
+ language: string;
802
+ }
803
+
804
+ const Settings: React.FC = () => {
805
+ const [settings, setSettings] =
806
+ useLocalStorage<AppSettings>(
807
+ 'app-settings',
808
+ {
809
+ theme: 'light',
810
+ notifications: true,
811
+ language: 'en'
812
+ }
813
+ );
814
+
815
+ const updateTheme = (theme: 'light' | 'dark') => {
816
+ setSettings({ ...settings, theme });
817
+ };
818
+
819
+ return (
820
+ <select
821
+ value={settings.theme}
822
+ onChange={(e) => updateTheme(
823
+ e.target.value as 'light' | 'dark'
824
+ )}
825
+ >
826
+ <option value="light">Light</option>
827
+ <option value="dark">Dark</option>
828
+ </select>
829
+ );
830
+ };
831
+ ```
832
+
833
+ </details>
834
+ } from 'msr-hooks';
835
+ import { useEffect } from 'react';
836
+
837
+ function Settings() {
838
+ const systemPrefersDark = useMediaQuery(
839
+ '(prefers-color-scheme: dark)'
840
+ );
841
+
842
+ const [settings, setSettings] = useLocalStorage(
843
+ 'app-settings',
844
+ {
845
+ theme: 'auto',
846
+ notifications: true,
847
+ language: 'en',
848
+ fontSize: 'medium',
849
+ soundEnabled: true
850
+ }
851
+ );
852
+
853
+ const [showAdvanced, toggleAdvanced] = useToggle(false);
854
+
855
+ // Auto theme based on system preference
856
+ const effectiveTheme = settings.theme === 'auto'
857
+ ? (systemPrefersDark ? 'dark' : 'light')
858
+ : settings.theme;
859
+
860
+ useEffect(() => {
861
+ document.body.className = effectiveTheme;
862
+ }, [effectiveTheme]);
863
+
864
+ const updateSetting = (key, value) => {
865
+ setSettings({ ...settings, [key]: value });
866
+ };
867
+
868
+ const resetToDefaults = () => {
869
+ setSettings({
870
+ theme: 'auto',
871
+ notifications: true,
872
+ language: 'en',
873
+ fontSize: 'medium',
874
+ soundEnabled: true
875
+ });
876
+ };
877
+
878
+ return (
879
+ <div className="settings">
880
+ <h2>Settings</h2>
881
+
882
+ <div className="setting-group">
883
+ <label>Theme</label>
884
+ <select
885
+ value={settings.theme}
886
+ onChange={(e) => updateSetting('theme', e.target.value)}
887
+ >
888
+ <option value="auto">Auto</option>
889
+ <option value="light">Light</option>
890
+ <option value="dark">Dark</option>
891
+ </select>
892
+ <small>Current: {effectiveTheme}</small>
893
+ </div>
894
+
895
+ <div className="setting-group">
896
+ <label>
897
+ <input
898
+ type="checkbox"
899
+ checked={settings.notifications}
900
+ onChange={(e) =>
901
+ updateSetting('notifications', e.target.checked)
902
+ }
903
+ />
904
+ Enable Notifications
905
+ </label>
906
+ </div>
907
+
908
+ <button onClick={toggleAdvanced}>
909
+ {showAdvanced ? 'Hide' : 'Show'} Advanced
910
+ </button>
911
+
912
+ {showAdvanced && (
913
+ <div className="advanced">
914
+ <div className="setting-group">
915
+ <label>Font Size</label>
916
+ <select
917
+ value={settings.fontSize}
918
+ onChange={(e) =>
919
+ updateSetting('fontSize', e.target.value)
920
+ }
921
+ >
922
+ <option value="small">Small</option>
923
+ <option value="medium">Medium</option>
924
+ <option value="large">Large</option>
925
+ </select>
926
+ </div>
927
+
928
+ <div className="setting-group">
929
+ <label>
930
+ <input
931
+ type="checkbox"
932
+ checked={settings.soundEnabled}
933
+ onChange={(e) =>
934
+ updateSetting('soundEnabled', e.target.checked)
935
+ }
936
+ />
937
+ Sound Effects
938
+ </label>
939
+ </div>
940
+ </div>
941
+ )}
942
+
943
+ <button onClick={resetToDefaults}>
944
+ Reset to Defaults
945
+ </button>
946
+ </div>
947
+ );
948
+ }
949
+ ```
950
+
951
+ </td>
952
+ <td width="50%">
953
+
954
+ **TypeScript**
955
+
956
+ ```typescript
957
+ import { useLocalStorage } from 'msr-hooks';
958
+
959
+ interface AppSettings {
960
+ theme: 'light' | 'dark';
961
+ notifications: boolean;
962
+ language: string;
963
+ }
964
+
965
+ const Settings: React.FC = () => {
966
+ const [settings, setSettings] =
967
+ useLocalStorage<AppSettings>(
968
+ 'app-settings',
969
+ {
970
+ theme: 'light',
971
+ notifications: true,
972
+ language: 'en'
973
+ }
974
+ );
975
+
976
+ const updateTheme = (theme: 'light' | 'dark') => {
977
+ setSettings({ ...settings, theme });
978
+ };
979
+
980
+ return (
981
+ <select
982
+ value={settings.theme}
983
+ onChange={(e) => updateTheme(
984
+ e.target.value as 'light' | 'dark'
985
+ )}
986
+ >
987
+ <option value="light">Light</option>
988
+ <option value="dark">Dark</option>
989
+ </select>
990
+ );
991
+ };
992
+ ```
993
+
994
+ </td>
995
+ </tr>
996
+ </table> value={settings.theme}
997
+ onChange={(e) => setSettings({ ...settings, theme: e.target.value })}
998
+ >
999
+ <option value="light">Light</option>
1000
+ <option value="dark">Dark</option>
1001
+ </select>
1002
+ </div>
1003
+ );
1004
+ }
1005
+ ```
1006
+
1007
+ ```typescript
1008
+ // TypeScript
1009
+ import { useLocalStorage } from 'msr-hooks';
1010
+
1011
+ interface AppSettings {
1012
+ theme: 'light' | 'dark';
1013
+ notifications: boolean;
1014
+ language: string;
1015
+ }
1016
+
1017
+ const Settings: React.FC = () => {
1018
+ const [settings, setSettings] = useLocalStorage<AppSettings>(
1019
+ 'app-settings',
1020
+ { theme: 'light', notifications: true, language: 'en' }
1021
+ );
1022
+
1023
+ const updateTheme = (theme: 'light' | 'dark') => {
1024
+ setSettings({ ...settings, theme });
1025
+ };
1026
+
1027
+ return (
1028
+ <select value={settings.theme} onChange={(e) => updateTheme(e.target.value as 'light' | 'dark')}>
1029
+ <option value="light">Light</option>
1030
+ <option value="dark">Dark</option>
1031
+ </select>
1032
+ );
1033
+ };
1034
+ ```
1035
+
1036
+ ---
1037
+
1038
+ ## 🔍 API Reference
1039
+
1040
+ ### useEffectAfterMount
1041
+ ```typescript
1042
+ useEffectAfterMount(effect: () => void | (() => void), deps?: DependencyList): void
1043
+ ```
1044
+ Run an effect only after the component has mounted (skips first render).
1045
+
1046
+ ### useDebounce
1047
+ ```typescript
1048
+ useDebounce<T>(value: T, delay?: number): T
1049
+ ```
1050
+ Debounce a changing value. Default delay: 300ms.
1051
+
1052
+ ### useThrottle
1053
+ ```typescript
1054
+ useThrottle<T>(value: T, limit?: number): T
1055
+ ```
1056
+ Throttle a changing value. Default limit: 500ms.
1057
+
1058
+ ### usePrevious
1059
+ ```typescript
1060
+ usePrevious<T>(value: T): T | undefined
1061
+ ```
1062
+ Get the previous value from the last render.
1063
+
1064
+ ### useToggle
1065
+ ```typescript
1066
+ useToggle(initial?: boolean): [boolean, () => void, () => void, () => void]
1067
+ ```
1068
+ Returns `[value, toggle, setTrue, setFalse]`.
1069
+
1070
+ ### useLocalStorage
1071
+ ```typescript
1072
+ useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void]
1073
+ ```
1074
+ Sync state to localStorage with JSON parsing. SSR-safe.
1075
+
1076
+ ### useFetch
1077
+ ```typescript
1078
+ useFetch<T = any>(url: string, options?: RequestInit): {
1079
+ data: T | null;
1080
+ loading: boolean;
1081
+ error: Error | null;
1082
+ refetch: () => Promise<void>;
1083
+ }
1084
+ ```
1085
+ Data fetching with loading/error states.
1086
+
1087
+ ### useWindowSize
1088
+ ```typescript
1089
+ useWindowSize(): { width: number; height: number }
1090
+ ```
1091
+ Track window dimensions. SSR-safe.
1092
+
1093
+ ### useMediaQuery
1094
+ ```typescript
1095
+ useMediaQuery(query: string): boolean
1096
+ ```
1097
+ Returns boolean if media query matches.
1098
+
1099
+ ### useClipboard
1100
+ ```typescript
1101
+ useClipboard(): [(text: string) => Promise<void>, boolean]
1102
+ ```
1103
+ Returns `[copyFn, isCopied]`.
1104
+
1105
+ ### useInterval
1106
+ ```typescript
1107
+ useInterval(callback: () => void, delay: number | null): void
1108
+ ```
1109
+ Declarative interval. Pass `null` as delay to pause.
1110
+
1111
+ ### useTimeout
1112
+ ```typescript
1113
+ useTimeout(callback: () => void, delay: number | null): void
1114
+ ```
1115
+ Declarative timeout. Pass `null` as delay to cancel.
1116
+
1117
+ ### useIntersectionObserver
1118
+ ```typescript
1119
+ useIntersectionObserver(options?: IntersectionObserverInit): [RefObject<HTMLElement>, boolean]
1120
+ ```
1121
+ Returns `[ref, isIntersecting]`.
1122
+
1123
+ ### useClickOutsideObject
1124
+ ```typescript
1125
+ useClickOutsideObject(
1126
+ ref: RefObject<HTMLElement>,
1127
+ handler: () => void,
1128
+ dontReactTo?: string,
1129
+ excludeRef?: RefObject<HTMLElement>
1130
+ ): void
1131
+ ```
1132
+ Detect clicks outside an element.
1133
+
1134
+ ### useEscapeKey
1135
+ ```typescript
1136
+ useEscapeKey(handler: () => void): void
1137
+ ```
1138
+ Trigger callback on Escape key.
1139
+
1140
+ ### useKeyboardNavigation
1141
+ ```typescript
1142
+ useKeyboardNavigation(config: {
1143
+ selectedIndex: number | null;
1144
+ handleSelect: (index: number | null) => void;
1145
+ totalBytes: number;
1146
+ bytesPerRow: number;
1147
+ }): void
1148
+ ```
1149
+ Arrow key navigation for grid structures.
1150
+
1151
+ ### usePreventZoom
1152
+ ```typescript
1153
+ usePreventZoom(scrollCheck?: boolean, keyboardCheck?: boolean): void
1154
+ ```
1155
+ Prevent browser zoom. Both default to `true`.
1156
+
1157
+ ### useParentWidth
1158
+ ```typescript
1159
+ useParentWidth(): {
1160
+ parentWidth: number | null;
1161
+ childRef: RefObject<HTMLDivElement>;
1162
+ }
1163
+ ```
1164
+ Get parent element width with ResizeObserver.
1165
+
1166
+ ### useResize
1167
+ ```typescript
1168
+ useResize(config: {
1169
+ defaultSize: number;
1170
+ minSize?: number;
1171
+ maxSize?: number;
1172
+ }): {
1173
+ size: number;
1174
+ setSize: (size: number) => void;
1175
+ isDragging: boolean;
1176
+ setIsDragging: (dragging: boolean) => void;
1177
+ handleMouseDown: (e: React.MouseEvent) => void;
1178
+ handleMouseUp: () => void;
1179
+ }
1180
+ ```
1181
+ Manage resizable element state.
1182
+
1183
+ ### useChangeIconColor
1184
+ ```typescript
1185
+ useChangeIconColor(color?: string): void
1186
+ ```
1187
+ Change favicon color dynamically. Default: `#000000`.
1188
+
1189
+ ---
1190
+
1191
+ ## 🎯 Best Practices
1192
+
1193
+ ### Tree Shaking
1194
+ Import only what you need for optimal bundle size:
1195
+
1196
+ ```javascript
1197
+ // ✅ Good - Tree shakeable
1198
+ import { useDebounce, useToggle } from 'msr-hooks';
1199
+
1200
+ // ❌ Avoid - Imports everything
1201
+ import * as hooks from 'msr-hooks';
1202
+ ```
1203
+
1204
+ ### TypeScript Usage
1205
+ Leverage full type safety:
1206
+
1207
+ ```typescript
1208
+ import { useFetch, useLocalStorage } from 'msr-hooks';
1209
+
1210
+ // Generic types are inferred
1211
+ const { data } = useFetch<User[]>('/api/users');
1212
+ const [count] = useLocalStorage<number>('count', 0);
1213
+ ```
1214
+
1215
+ ### SSR Compatibility
1216
+ All hooks with browser APIs include SSR guards:
1217
+
1218
+ ```javascript
1219
+ // Safe to use in Next.js, Gatsby, etc.
1220
+ const { width } = useWindowSize(); // Returns { width: 0, height: 0 } on server
1221
+ const [theme] = useLocalStorage('theme', 'light'); // Safe on server
1222
+ ```
1223
+
1224
+ ---
1225
+
1226
+ ## 📄 License
1227
+
1228
+ MIT © MSR
1229
+
1230
+ ---
1231
+
1232
+ ## 🤝 Contributing
1233
+
1234
+ Contributions are welcome! Please feel free to submit a Pull Request.
1235
+
1236
+ ---
1237
+
1238
+ ## 🔗 Links
1239
+
1240
+ - [npm Package](https://www.npmjs.com/package/msr-hooks)
1241
+ - [GitHub Repository](https://github.com/yourusername/msr-hooks)
1242
+ - [Issue Tracker](https://github.com/yourusername/msr-hooks/issues)
1243
+
1244
+ ---
1245
+
1246
+ <div align="center">
1247
+
1248
+ **Made with ❤️ for the React community**
1249
+
1250
+ If you find this useful, please give it a ⭐️
1251
+
1252
+ </div>