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.
- package/README.md +1252 -0
- package/package.json +50 -0
- package/src/hooks/index.js +53 -0
- package/src/hooks/useAsyncEffect/index.js +1 -0
- package/src/hooks/useAsyncEffect/useAsyncEffect.d.ts +6 -0
- package/src/hooks/useAsyncEffect/useAsyncEffect.js +31 -0
- package/src/hooks/useChangeIconColor/index.js +1 -0
- package/src/hooks/useChangeIconColor/useChangeIconColor.d.ts +1 -0
- package/src/hooks/useChangeIconColor/useChangeIconColor.js +39 -0
- package/src/hooks/useClickOutsideObject/index.js +1 -0
- package/src/hooks/useClickOutsideObject/useClickOutsideObject.d.ts +8 -0
- package/src/hooks/useClickOutsideObject/useClickOutsideObject.js +28 -0
- package/src/hooks/useClipboard/index.js +1 -0
- package/src/hooks/useClipboard/useClipboard.d.ts +4 -0
- package/src/hooks/useClipboard/useClipboard.js +27 -0
- package/src/hooks/useDebounce/index.js +1 -0
- package/src/hooks/useDebounce/useDebounce.d.ts +1 -0
- package/src/hooks/useDebounce/useDebounce.js +18 -0
- package/src/hooks/useDeepCompareEffect/index.js +1 -0
- package/src/hooks/useDeepCompareEffect/useDeepCompareEffect.d.ts +3 -0
- package/src/hooks/useDeepCompareEffect/useDeepCompareEffect.js +40 -0
- package/src/hooks/useDocumentVisibility/index.js +1 -0
- package/src/hooks/useDocumentVisibility/useDocumentVisibility.d.ts +1 -0
- package/src/hooks/useDocumentVisibility/useDocumentVisibility.js +19 -0
- package/src/hooks/useEffectAfterMount/index.js +1 -0
- package/src/hooks/useEffectAfterMount/useEffectAfterMount.d.ts +4 -0
- package/src/hooks/useEffectAfterMount/useEffectAfterMount.js +20 -0
- package/src/hooks/useElementScrollProgress/index.js +1 -0
- package/src/hooks/useElementScrollProgress/useElementScrollProgress.d.ts +5 -0
- package/src/hooks/useElementScrollProgress/useElementScrollProgress.js +37 -0
- package/src/hooks/useEscapeKey/index.js +1 -0
- package/src/hooks/useEscapeKey/useEscapeKey.d.ts +1 -0
- package/src/hooks/useEscapeKey/useEscapeKey.js +22 -0
- package/src/hooks/useEventListener/index.js +1 -0
- package/src/hooks/useEventListener/useEventListener.d.ts +8 -0
- package/src/hooks/useEventListener/useEventListener.js +25 -0
- package/src/hooks/useFetch/index.js +1 -0
- package/src/hooks/useFetch/useFetch.d.ts +11 -0
- package/src/hooks/useFetch/useFetch.js +35 -0
- package/src/hooks/useHoverIntent/index.js +1 -0
- package/src/hooks/useHoverIntent/useHoverIntent.d.ts +6 -0
- package/src/hooks/useHoverIntent/useHoverIntent.js +81 -0
- package/src/hooks/useIntersectionObserver/index.js +1 -0
- package/src/hooks/useIntersectionObserver/useIntersectionObserver.d.ts +5 -0
- package/src/hooks/useIntersectionObserver/useIntersectionObserver.js +25 -0
- package/src/hooks/useInterval/index.js +1 -0
- package/src/hooks/useInterval/useInterval.d.ts +4 -0
- package/src/hooks/useInterval/useInterval.js +23 -0
- package/src/hooks/useIsomorphicLayoutEffect/index.js +1 -0
- package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.d.ts +3 -0
- package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.js +3 -0
- package/src/hooks/useKeyPressSequence/index.js +1 -0
- package/src/hooks/useKeyPressSequence/useKeyPressSequence.d.ts +5 -0
- package/src/hooks/useKeyPressSequence/useKeyPressSequence.js +52 -0
- package/src/hooks/useKeyboardNavigation/index.js +1 -0
- package/src/hooks/useKeyboardNavigation/useKeyboardNavigation.d.ts +10 -0
- package/src/hooks/useKeyboardNavigation/useKeyboardNavigation.js +50 -0
- package/src/hooks/useLocalStorage/index.js +1 -0
- package/src/hooks/useLocalStorage/useLocalStorage.d.ts +4 -0
- package/src/hooks/useLocalStorage/useLocalStorage.js +44 -0
- package/src/hooks/useLockBodyScroll/index.js +1 -0
- package/src/hooks/useLockBodyScroll/useLockBodyScroll.d.ts +1 -0
- package/src/hooks/useLockBodyScroll/useLockBodyScroll.js +25 -0
- package/src/hooks/useMediaQuery/index.js +1 -0
- package/src/hooks/useMediaQuery/useMediaQuery.d.ts +1 -0
- package/src/hooks/useMediaQuery/useMediaQuery.js +24 -0
- package/src/hooks/useNetworkStatus/index.js +1 -0
- package/src/hooks/useNetworkStatus/useNetworkStatus.d.ts +7 -0
- package/src/hooks/useNetworkStatus/useNetworkStatus.js +47 -0
- package/src/hooks/usePageLeave/index.js +1 -0
- package/src/hooks/usePageLeave/usePageLeave.d.ts +1 -0
- package/src/hooks/usePageLeave/usePageLeave.js +27 -0
- package/src/hooks/useParentWidth/index.js +1 -0
- package/src/hooks/useParentWidth/useParentWidth.d.ts +8 -0
- package/src/hooks/useParentWidth/useParentWidth.js +30 -0
- package/src/hooks/usePortal/index.js +1 -0
- package/src/hooks/usePortal/usePortal.d.ts +1 -0
- package/src/hooks/usePortal/usePortal.js +33 -0
- package/src/hooks/usePrefersReducedMotion/index.js +1 -0
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.d.ts +1 -0
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.js +24 -0
- package/src/hooks/usePreventZoom/index.js +1 -0
- package/src/hooks/usePreventZoom/usePreventZoom.d.ts +4 -0
- package/src/hooks/usePreventZoom/usePreventZoom.js +39 -0
- package/src/hooks/usePrevious/index.js +1 -0
- package/src/hooks/usePrevious/usePrevious.d.ts +1 -0
- package/src/hooks/usePrevious/usePrevious.js +16 -0
- package/src/hooks/useResize/index.js +1 -0
- package/src/hooks/useResize/useResize.d.ts +16 -0
- package/src/hooks/useResize/useResize.js +32 -0
- package/src/hooks/useSpringValue/index.js +1 -0
- package/src/hooks/useSpringValue/useSpringValue.d.ts +4 -0
- package/src/hooks/useSpringValue/useSpringValue.js +66 -0
- package/src/hooks/useStateHistory/index.js +1 -0
- package/src/hooks/useStateHistory/useStateHistory.d.ts +17 -0
- package/src/hooks/useStateHistory/useStateHistory.js +70 -0
- package/src/hooks/useThrottle/index.js +1 -0
- package/src/hooks/useThrottle/useThrottle.d.ts +1 -0
- package/src/hooks/useThrottle/useThrottle.js +25 -0
- package/src/hooks/useTimeout/index.js +1 -0
- package/src/hooks/useTimeout/useTimeout.d.ts +4 -0
- package/src/hooks/useTimeout/useTimeout.js +22 -0
- package/src/hooks/useToggle/index.js +1 -0
- package/src/hooks/useToggle/useToggle.d.ts +3 -0
- package/src/hooks/useToggle/useToggle.js +16 -0
- package/src/hooks/useUndoRedo/index.js +1 -0
- package/src/hooks/useUndoRedo/useUndoRedo.d.ts +18 -0
- package/src/hooks/useUndoRedo/useUndoRedo.js +49 -0
- package/src/hooks/useWindowSize/index.js +1 -0
- package/src/hooks/useWindowSize/useWindowSize.d.ts +1 -0
- package/src/hooks/useWindowSize/useWindowSize.js +29 -0
- package/src/index.d.ts +1 -0
- 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
|
+
[](https://www.npmjs.com/package/msr-hooks)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](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>
|