@alessiofrittoli/react-hooks 3.2.0-alpha.3 → 3.2.1
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 +1249 -1249
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/license.md +20 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,1249 +1,1249 @@
|
|
|
1
|
-
# React Hooks 🪝
|
|
2
|
-
|
|
3
|
-
[![NPM Latest Version][version-badge]][npm-url] [![Coverage Status][coverage-badge]][coverage-url] [![Socket Status][socket-badge]][socket-url] [![NPM Monthly Downloads][downloads-badge]][npm-url] [![Dependencies][deps-badge]][deps-url]
|
|
4
|
-
|
|
5
|
-
[![GitHub Sponsor][sponsor-badge]][sponsor-url]
|
|
6
|
-
|
|
7
|
-
[version-badge]: https://img.shields.io/npm/v/%40alessiofrittoli%2Freact-hooks
|
|
8
|
-
[npm-url]: https://npmjs.org/package/%40alessiofrittoli%2Freact-hooks
|
|
9
|
-
[coverage-badge]: https://coveralls.io/repos/github/alessiofrittoli/react-hooks/badge.svg
|
|
10
|
-
[coverage-url]: https://coveralls.io/github/alessiofrittoli/react-hooks
|
|
11
|
-
[socket-badge]: https://socket.dev/api/badge/npm/package/@alessiofrittoli/react-hooks
|
|
12
|
-
[socket-url]: https://socket.dev/npm/package/@alessiofrittoli/react-hooks/overview
|
|
13
|
-
[downloads-badge]: https://img.shields.io/npm/dm/%40alessiofrittoli%2Freact-hooks.svg
|
|
14
|
-
[deps-badge]: https://img.shields.io/librariesio/release/npm/%40alessiofrittoli%2Freact-hooks
|
|
15
|
-
[deps-url]: https://libraries.io/npm/%40alessiofrittoli%2Freact-hooks
|
|
16
|
-
|
|
17
|
-
[sponsor-badge]: https://img.shields.io/static/v1?label=Fund%20this%20package&message=%E2%9D%A4&logo=GitHub&color=%23DB61A2
|
|
18
|
-
[sponsor-url]: https://github.com/sponsors/alessiofrittoli
|
|
19
|
-
|
|
20
|
-
## TypeScript React utility Hooks
|
|
21
|
-
|
|
22
|
-
### Table of Contents
|
|
23
|
-
|
|
24
|
-
- [Getting started](#getting-started)
|
|
25
|
-
- [ESLint Configuration](#eslint-configuration)
|
|
26
|
-
- [API Reference](#api-reference)
|
|
27
|
-
- [Browser API](#browser-api)
|
|
28
|
-
- [`useStorage`](#usestorage)
|
|
29
|
-
- [`useLocalStorage`](#uselocalstorage)
|
|
30
|
-
- [`useSessionStorage`](#usesessionstorage)
|
|
31
|
-
- [`useMediaQuery`](#usemediaquery)
|
|
32
|
-
- [`useDarkMode`](#usedarkmode)
|
|
33
|
-
- [`useIsPortrait`](#useisportrait)
|
|
34
|
-
- [DOM API](#dom-api)
|
|
35
|
-
- [`useScrollBlock`](#usescrollblock)
|
|
36
|
-
- [`useFocusTrap`](#usefocustrap)
|
|
37
|
-
- [`useInView`](#useinview)
|
|
38
|
-
- [Miscellaneous](#miscellaneous)
|
|
39
|
-
- [`useIsClient`](#useisclient)
|
|
40
|
-
- [`useIsFirstRender`](#useisfirstrender)
|
|
41
|
-
- [`useUpdateEffect`](#useupdateeffect)
|
|
42
|
-
- [`usePagination`](#usepagination)
|
|
43
|
-
- [Development](#development)
|
|
44
|
-
- [Install depenendencies](#install-depenendencies)
|
|
45
|
-
- [Build the source code](#build-the-source-code)
|
|
46
|
-
- [ESLint](#eslint)
|
|
47
|
-
- [Jest](#jest)
|
|
48
|
-
- [Contributing](#contributing)
|
|
49
|
-
- [Security](#security)
|
|
50
|
-
- [Credits](#made-with-)
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
### Getting started
|
|
55
|
-
|
|
56
|
-
Run the following command to start using `react-hooks` in your projects:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
npm i @alessiofrittoli/react-hooks
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
or using `pnpm`
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
pnpm i @alessiofrittoli/react-hooks
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
### ESLint Configuration
|
|
71
|
-
|
|
72
|
-
This library may define and exports hooks that requires additional ESLint configuration for your project such as [`useUpdateEffect`](#useupdateeffect).
|
|
73
|
-
|
|
74
|
-
Simply imports recommended configuration from `@alessiofrittoli/react-hooks/eslint` and add them to your ESLint configuration like so:
|
|
75
|
-
|
|
76
|
-
```mjs
|
|
77
|
-
import { config as AFReactHooksEslint } from '@alessiofrittoli/react-hooks/eslint'
|
|
78
|
-
|
|
79
|
-
/** @type {import('eslint').Linter.Config[]} */
|
|
80
|
-
const config = [
|
|
81
|
-
...AFReactHooksEslint.recommended,
|
|
82
|
-
// ... other configurations
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
export default config
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
### API Reference
|
|
92
|
-
|
|
93
|
-
#### Browser API
|
|
94
|
-
|
|
95
|
-
##### Storage
|
|
96
|
-
|
|
97
|
-
The following storage hooks use Storage Utilities from [`@alessiofrittoli/web-utils`](https://npmjs.com/package/@alessiofrittoli/web-utils#storage-utilities) adding a React oriented implementation.
|
|
98
|
-
|
|
99
|
-
###### `useStorage`
|
|
100
|
-
|
|
101
|
-
Easly handle Local or Session Storage State.
|
|
102
|
-
|
|
103
|
-
<details>
|
|
104
|
-
|
|
105
|
-
<summary style="cursor:pointer">Type parameters</summary>
|
|
106
|
-
|
|
107
|
-
| Parameter | Type | Default | Description |
|
|
108
|
-
|-----------|------|---------|-------------|
|
|
109
|
-
| `T` | `any` | `string` | A custom type applied to the stored item. |
|
|
110
|
-
|
|
111
|
-
</details>
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
<details>
|
|
116
|
-
|
|
117
|
-
<summary style="cursor:pointer">Parameters</summary>
|
|
118
|
-
|
|
119
|
-
| Parameter | Type | Default | Description |
|
|
120
|
-
|-----------|------|---------|-------------|
|
|
121
|
-
| `key` | `string` | - | The storage item key. |
|
|
122
|
-
| `initial` | `T` | - | The storage item initial value. |
|
|
123
|
-
| `type` | `local\|session` | local | (Optional) The storage API to use. |
|
|
124
|
-
|
|
125
|
-
</details>
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
<details>
|
|
130
|
-
|
|
131
|
-
<summary style="cursor:pointer">Returns</summary>
|
|
132
|
-
|
|
133
|
-
Type: `[ Value<T>, SetValue<Value<T>> ]`
|
|
134
|
-
|
|
135
|
-
A tuple with the stored item value or initial value and the setter function.
|
|
136
|
-
|
|
137
|
-
</details>
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
<details>
|
|
142
|
-
|
|
143
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
144
|
-
|
|
145
|
-
###### Importing the hooks
|
|
146
|
-
|
|
147
|
-
```tsx
|
|
148
|
-
import {
|
|
149
|
-
useStorage, useLocalStorage, useSessionStorage
|
|
150
|
-
} from '@alessiofrittoli/react-hooks'
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
###### Reading item value from storage
|
|
156
|
-
|
|
157
|
-
```tsx
|
|
158
|
-
'use client'
|
|
159
|
-
|
|
160
|
-
import { useStorage } from '@alessiofrittoli/react-hooks'
|
|
161
|
-
|
|
162
|
-
type Locale = 'it' | 'en'
|
|
163
|
-
|
|
164
|
-
const storage = 'local' // or 'session'
|
|
165
|
-
const defaultLocale = 'it'
|
|
166
|
-
|
|
167
|
-
export const SomeComponent: React.FC = () => {
|
|
168
|
-
|
|
169
|
-
const [ userLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
|
|
170
|
-
|
|
171
|
-
return (
|
|
172
|
-
...
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
###### Updating storage item value
|
|
181
|
-
|
|
182
|
-
```tsx
|
|
183
|
-
'use client'
|
|
184
|
-
|
|
185
|
-
import { useCallback } from 'react'
|
|
186
|
-
import { useStorage } from '@alessiofrittoli/react-hooks'
|
|
187
|
-
|
|
188
|
-
type Locale = 'it' | 'en'
|
|
189
|
-
|
|
190
|
-
const storage = 'local' // or 'session'
|
|
191
|
-
const defaultLocale = 'it'
|
|
192
|
-
|
|
193
|
-
export const LanguageSwitcher: React.FC = () => {
|
|
194
|
-
|
|
195
|
-
const [ userLocale, setUserLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
|
|
196
|
-
|
|
197
|
-
const clickHandler = useCallback( () => {
|
|
198
|
-
setUserLocale( 'en' )
|
|
199
|
-
}, [ setUserLocale ] )
|
|
200
|
-
|
|
201
|
-
return (
|
|
202
|
-
...
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
###### Deleting storage item
|
|
211
|
-
|
|
212
|
-
```tsx
|
|
213
|
-
'use client'
|
|
214
|
-
|
|
215
|
-
import { useCallback } from 'react'
|
|
216
|
-
import { useStorage } from '@alessiofrittoli/react-hooks'
|
|
217
|
-
|
|
218
|
-
type Locale = 'it' | 'en'
|
|
219
|
-
|
|
220
|
-
const storage = 'local' // or 'session'
|
|
221
|
-
const defaultLocale = 'it'
|
|
222
|
-
|
|
223
|
-
export const LanguageSwitcher: React.FC = () => {
|
|
224
|
-
|
|
225
|
-
const [ userLocale, setUserLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
|
|
226
|
-
|
|
227
|
-
const deleteHandler = useCallback( () => {
|
|
228
|
-
setUserLocale( null )
|
|
229
|
-
// or
|
|
230
|
-
setUserLocale( undefined )
|
|
231
|
-
// or
|
|
232
|
-
setUserLocale( '' )
|
|
233
|
-
}, [ setUserLocale ] )
|
|
234
|
-
|
|
235
|
-
return (
|
|
236
|
-
...
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
</details>
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
###### `useLocalStorage`
|
|
247
|
-
|
|
248
|
-
Shortcut React Hook for [`useStorage`](#usestorage).
|
|
249
|
-
|
|
250
|
-
Applies the same API Reference.
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
###### `useSessionStorage`
|
|
255
|
-
|
|
256
|
-
Shortcut React Hook for [`useStorage`](#usestorage).
|
|
257
|
-
|
|
258
|
-
Applies the same API Reference.
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
##### `useMediaQuery`
|
|
263
|
-
|
|
264
|
-
Get Document Media matches and listen for changes.
|
|
265
|
-
|
|
266
|
-
<details>
|
|
267
|
-
|
|
268
|
-
<summary style="cursor:pointer">Parameters</summary>
|
|
269
|
-
|
|
270
|
-
| Parameter | Type | Default | Description |
|
|
271
|
-
|-----------|----------|---------|-------------|
|
|
272
|
-
| `query` | `string` | - | A string specifying the media query to parse into a `MediaQueryList`. |
|
|
273
|
-
| `options` | `UseMediaQueryOptions\|UseMediaQueryStateOptions` | - | An object defining custom options. |
|
|
274
|
-
| `options.updateState` | `boolean` | `true` | Indicates whether the hook will dispatch a React state update when the given `query` change event get dispatched. |
|
|
275
|
-
| `options.onChange` | `OnChangeHandler` | - | A custom callback that will be invoked on initial page load and when the given `query` change event get dispatched. |
|
|
276
|
-
| | | | This callback is required if `updateState` is set to `false`. |
|
|
277
|
-
|
|
278
|
-
</details>
|
|
279
|
-
|
|
280
|
-
---
|
|
281
|
-
|
|
282
|
-
<details>
|
|
283
|
-
|
|
284
|
-
<summary style="cursor:pointer">Returns</summary>
|
|
285
|
-
|
|
286
|
-
Type: `boolean|void`
|
|
287
|
-
|
|
288
|
-
- `true` or `false` if the document currently matches the media query list or not.
|
|
289
|
-
- `void` if `updateState` is set to `false`.
|
|
290
|
-
|
|
291
|
-
</details>
|
|
292
|
-
|
|
293
|
-
---
|
|
294
|
-
|
|
295
|
-
<details>
|
|
296
|
-
|
|
297
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
298
|
-
|
|
299
|
-
###### Check if user device prefers dark color scheme
|
|
300
|
-
|
|
301
|
-
```tsx
|
|
302
|
-
import { useMediaQuery } from '@alessiofrittoli/react-hooks'
|
|
303
|
-
|
|
304
|
-
const isDarkOS = useMediaQuery( '(prefers-color-scheme: dark)' )
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
---
|
|
308
|
-
|
|
309
|
-
###### Listen changes with no state updates
|
|
310
|
-
|
|
311
|
-
```tsx
|
|
312
|
-
import { useMediaQuery } from '@alessiofrittoli/react-hooks'
|
|
313
|
-
|
|
314
|
-
useMediaQuery( '(prefers-color-scheme: dark)', {
|
|
315
|
-
updateState: false,
|
|
316
|
-
onChange( matches ) {
|
|
317
|
-
console.log( 'is dark OS?', matches )
|
|
318
|
-
}
|
|
319
|
-
} )
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
</details>
|
|
323
|
-
|
|
324
|
-
---
|
|
325
|
-
|
|
326
|
-
##### `useConnection`
|
|
327
|
-
|
|
328
|
-
Docs coming soon
|
|
329
|
-
|
|
330
|
-
---
|
|
331
|
-
|
|
332
|
-
##### `useDarkMode`
|
|
333
|
-
|
|
334
|
-
Easily manage dark mode with full respect for user device preferences.
|
|
335
|
-
|
|
336
|
-
This hook is user-oriented and built to honor system-level color scheme settings:
|
|
337
|
-
|
|
338
|
-
- If the device prefers a dark color scheme, dark mode is automatically enabled on first load.
|
|
339
|
-
- If the user enables/disables dark mode via a web widget, the preference is stored in `localStorage` under the key `dark-mode`.
|
|
340
|
-
- If the device color scheme preference changes (e.g. via OS settings), that change takes precedence and is stored for future visits.
|
|
341
|
-
|
|
342
|
-
<details>
|
|
343
|
-
|
|
344
|
-
<summary style="cursor:pointer">Parameters</summary>
|
|
345
|
-
|
|
346
|
-
| Parameter | Type | Description |
|
|
347
|
-
|-----------|------|-------------|
|
|
348
|
-
| `options` | `UseDarkModeOptions` | (Optional) Configuration object for the hook. |
|
|
349
|
-
| `options.initial` | `boolean` | (Optional) The fallback value to use if no preference is saved in `localStorage`. Defaults to `true` if the device prefers dark mode. |
|
|
350
|
-
| `options.docClassNames` | `[dark: string, light: string]` | (Optional) Array of class names to toggle on the `<html>` element, e.g. `['dark', 'light']`. |
|
|
351
|
-
|
|
352
|
-
</details>
|
|
353
|
-
|
|
354
|
-
---
|
|
355
|
-
|
|
356
|
-
<details>
|
|
357
|
-
|
|
358
|
-
<summary style="cursor:pointer">Returns</summary>
|
|
359
|
-
|
|
360
|
-
Type: `UseDarkModeOutput`
|
|
361
|
-
|
|
362
|
-
An object containing utilities for managing dark mode:
|
|
363
|
-
|
|
364
|
-
- `isDarkMode`: `boolean` — Whether dark mode is currently enabled.
|
|
365
|
-
- `isDarkOS`: `boolean` — Whether the user's system prefers dark mode.
|
|
366
|
-
- `toggleDarkMode`: `() => void` — Toggles dark mode and saves the preference.
|
|
367
|
-
- `enableDarkMode`: `() => void` — Enables dark mode and saves the preference.
|
|
368
|
-
- `disableDarkMode`: `() => void` — Disables dark mode and saves the preference.
|
|
369
|
-
|
|
370
|
-
</details>
|
|
371
|
-
|
|
372
|
-
---
|
|
373
|
-
|
|
374
|
-
<details>
|
|
375
|
-
|
|
376
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
377
|
-
|
|
378
|
-
###### Basic usage
|
|
379
|
-
|
|
380
|
-
```tsx
|
|
381
|
-
'use client'
|
|
382
|
-
|
|
383
|
-
import { useDarkMode } from '@alessiofrittoli/react-hooks'
|
|
384
|
-
|
|
385
|
-
export const Component: React.FC = () => {
|
|
386
|
-
const { isDarkMode } = useDarkMode()
|
|
387
|
-
|
|
388
|
-
return (
|
|
389
|
-
<div>{ isDarkMode ? 'Dark mode enabled' : 'Dark mode disabled' }</div>
|
|
390
|
-
)
|
|
391
|
-
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
---
|
|
395
|
-
|
|
396
|
-
###### Update Document class names for CSS styling
|
|
397
|
-
|
|
398
|
-
```tsx
|
|
399
|
-
// Component.tsx
|
|
400
|
-
'use client'
|
|
401
|
-
|
|
402
|
-
import { useDarkMode } from '@alessiofrittoli/react-hooks'
|
|
403
|
-
|
|
404
|
-
export const Component: React.FC = () => {
|
|
405
|
-
const { isDarkMode } = useDarkMode( {
|
|
406
|
-
docClassNames: [ 'dark', 'light' ],
|
|
407
|
-
} )
|
|
408
|
-
|
|
409
|
-
return (
|
|
410
|
-
<div>{ isDarkMode ? 'Dark mode enabled' : 'Dark mode disabled' }</div>
|
|
411
|
-
)
|
|
412
|
-
}
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
```css
|
|
416
|
-
/* style.css */
|
|
417
|
-
.light {
|
|
418
|
-
color-scheme: light;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
.dark {
|
|
422
|
-
color-scheme: dark;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
.light body
|
|
426
|
-
{
|
|
427
|
-
color : black;
|
|
428
|
-
background: white;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
.dark body
|
|
432
|
-
{
|
|
433
|
-
color : white;
|
|
434
|
-
background: black;
|
|
435
|
-
}
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
---
|
|
439
|
-
|
|
440
|
-
###### Custom theme switcher
|
|
441
|
-
|
|
442
|
-
```tsx
|
|
443
|
-
'use client'
|
|
444
|
-
|
|
445
|
-
import { useDarkMode } from '@alessiofrittoli/react-hooks'
|
|
446
|
-
|
|
447
|
-
export const ThemeSwitcher: React.FC = () => {
|
|
448
|
-
const { isDarkMode, toggleDarkMode } = useDarkMode()
|
|
449
|
-
|
|
450
|
-
return (
|
|
451
|
-
<button onClick={ toggleDarkMode }>
|
|
452
|
-
{ isDarkMode ? '🌙' : '☀️' }
|
|
453
|
-
</button>
|
|
454
|
-
)
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
---
|
|
459
|
-
|
|
460
|
-
###### Sync Document theme-color for consistent browser styling
|
|
461
|
-
|
|
462
|
-
Browsers automatically apply colorization using:
|
|
463
|
-
|
|
464
|
-
```html
|
|
465
|
-
<meta name='theme-color' media='(prefers-color-scheme: dark)' />
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
This works based on the OS preference — *not your site theme*. That can cause mismatches if, for example, the system is in dark mode but the user disabled dark mode via a web toggle.
|
|
469
|
-
|
|
470
|
-
To ensure consistency, `useDarkMode` updates these meta tags dynamically based on the actual mode.
|
|
471
|
-
|
|
472
|
-
Just make sure to define both `light` and `dark` theme-color tags in your document:
|
|
473
|
-
|
|
474
|
-
```html
|
|
475
|
-
<head>
|
|
476
|
-
<meta name='theme-color' media='(prefers-color-scheme: light)' content='lime'>
|
|
477
|
-
<meta name='theme-color' media='(prefers-color-scheme: dark)' content='aqua'>
|
|
478
|
-
</head>
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
</details>
|
|
482
|
-
|
|
483
|
-
---
|
|
484
|
-
|
|
485
|
-
##### `useIsPortrait`
|
|
486
|
-
|
|
487
|
-
Check if device is portrait oriented.
|
|
488
|
-
|
|
489
|
-
React State get updated when device orientation changes.
|
|
490
|
-
|
|
491
|
-
<details>
|
|
492
|
-
|
|
493
|
-
<summary style="cursor:pointer">Returns</summary>
|
|
494
|
-
|
|
495
|
-
Type: `boolean`
|
|
496
|
-
|
|
497
|
-
- `true` if the device is portrait oriented.
|
|
498
|
-
- `false` otherwise.
|
|
499
|
-
|
|
500
|
-
</details>
|
|
501
|
-
|
|
502
|
-
---
|
|
503
|
-
|
|
504
|
-
<details>
|
|
505
|
-
|
|
506
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
507
|
-
|
|
508
|
-
###### Check if user device is in landscape
|
|
509
|
-
|
|
510
|
-
```tsx
|
|
511
|
-
import { useIsPortrait } from '@alessiofrittoli/react-hooks'
|
|
512
|
-
|
|
513
|
-
const isLandscape = ! useIsPortrait()
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
</details>
|
|
517
|
-
|
|
518
|
-
---
|
|
519
|
-
|
|
520
|
-
#### DOM API
|
|
521
|
-
|
|
522
|
-
##### `useScrollBlock`
|
|
523
|
-
|
|
524
|
-
Prevent Element overflow.
|
|
525
|
-
|
|
526
|
-
<details>
|
|
527
|
-
|
|
528
|
-
<summary style="cursor:pointer">Parameters</summary>
|
|
529
|
-
|
|
530
|
-
| Parameter | Type | Default | Description |
|
|
531
|
-
|-----------|------|---------|-------------|
|
|
532
|
-
| `target` | `React.RefObject<HTMLElement\|null>` | `Document.documentElement` | (Optional) The React RefObject target HTMLElement. |
|
|
533
|
-
|
|
534
|
-
</details>
|
|
535
|
-
|
|
536
|
-
---
|
|
537
|
-
|
|
538
|
-
<details>
|
|
539
|
-
|
|
540
|
-
<summary style="cursor:pointer">Returns</summary>
|
|
541
|
-
|
|
542
|
-
Type: `[ () => void, () => void ]`
|
|
543
|
-
|
|
544
|
-
A tuple with block and restore scroll callbacks.
|
|
545
|
-
|
|
546
|
-
</details>
|
|
547
|
-
|
|
548
|
-
---
|
|
549
|
-
|
|
550
|
-
<details>
|
|
551
|
-
|
|
552
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
553
|
-
|
|
554
|
-
###### Block Document Overflow
|
|
555
|
-
|
|
556
|
-
```tsx
|
|
557
|
-
import { useScrollBlock } from '@alessiofrittoli/react-hooks'
|
|
558
|
-
|
|
559
|
-
const [ blockScroll, restoreScroll ] = useScrollBlock()
|
|
560
|
-
|
|
561
|
-
const openPopUpHandler = useCallback( () => {
|
|
562
|
-
...
|
|
563
|
-
blockScroll()
|
|
564
|
-
}, [ blockScroll ] )
|
|
565
|
-
|
|
566
|
-
const closePopUpHandler = useCallback( () => {
|
|
567
|
-
...
|
|
568
|
-
restoreScroll()
|
|
569
|
-
}, [ restoreScroll ] )
|
|
570
|
-
|
|
571
|
-
...
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
---
|
|
575
|
-
|
|
576
|
-
###### Block HTML Element Overflow
|
|
577
|
-
|
|
578
|
-
```tsx
|
|
579
|
-
const elementRef = useRef<HTMLDivElement>( null )
|
|
580
|
-
|
|
581
|
-
const [ blockScroll, restoreScroll ] = useScrollBlock( elementRef )
|
|
582
|
-
|
|
583
|
-
const scrollBlockHandler = useCallback( () => {
|
|
584
|
-
...
|
|
585
|
-
blockScroll()
|
|
586
|
-
}, [ blockScroll ] )
|
|
587
|
-
|
|
588
|
-
const scrollRestoreHandler = useCallback( () => {
|
|
589
|
-
...
|
|
590
|
-
restoreScroll()
|
|
591
|
-
}, [ restoreScroll ] )
|
|
592
|
-
|
|
593
|
-
...
|
|
594
|
-
```
|
|
595
|
-
|
|
596
|
-
</details>
|
|
597
|
-
|
|
598
|
-
---
|
|
599
|
-
|
|
600
|
-
##### `useFocusTrap`
|
|
601
|
-
|
|
602
|
-
Trap focus inside the given HTML Element.
|
|
603
|
-
|
|
604
|
-
This comes pretty handy when rendering a modal that shouldn't be closed without a user required action.
|
|
605
|
-
|
|
606
|
-
<details>
|
|
607
|
-
|
|
608
|
-
<summary style="cursor:pointer">Parameters</summary>
|
|
609
|
-
|
|
610
|
-
| Parameter | Type | Description |
|
|
611
|
-
|-----------|------|-------------|
|
|
612
|
-
| `target` | `React.RefObject<HTMLElement\|null>` | The target HTMLElement React RefObject to trap focus within. |
|
|
613
|
-
| | | If no target is given, you must provide the target HTMLElement when calling `setFocusTrap`. |
|
|
614
|
-
|
|
615
|
-
</details>
|
|
616
|
-
|
|
617
|
-
---
|
|
618
|
-
|
|
619
|
-
<details>
|
|
620
|
-
|
|
621
|
-
<summary style="cursor:pointer">Returns</summary>
|
|
622
|
-
|
|
623
|
-
Type: `readonly [ SetFocusTrap, RestoreFocusTrap ]`
|
|
624
|
-
|
|
625
|
-
A tuple containing:
|
|
626
|
-
|
|
627
|
-
- `setFocusTrap`: A function to enable the focus trap. Optionally accept an HTMLElement as target.
|
|
628
|
-
- `restoreFocusTrap`: A function to restore the previous focus state.
|
|
629
|
-
|
|
630
|
-
</details>
|
|
631
|
-
|
|
632
|
-
---
|
|
633
|
-
|
|
634
|
-
<details>
|
|
635
|
-
|
|
636
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
637
|
-
|
|
638
|
-
###### Defining the target on hook initialization
|
|
639
|
-
|
|
640
|
-
```tsx
|
|
641
|
-
import { useFocusTrap } from '@alessiofrittoli/react-hooks'
|
|
642
|
-
|
|
643
|
-
const modalRef = useRef<HTMLDivElement>( null )
|
|
644
|
-
const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap( modalRef )
|
|
645
|
-
|
|
646
|
-
const modalOpenHandler = useCallback( () => {
|
|
647
|
-
if ( ! modalRef.current ) return
|
|
648
|
-
// ... open modal
|
|
649
|
-
setFocusTrap()
|
|
650
|
-
modalRef.current.focus() // focus the dialog so next tab will focus the next element inside the modal
|
|
651
|
-
}, [ setFocusTrap ] )
|
|
652
|
-
|
|
653
|
-
const modalCloseHandler = useCallback( () => {
|
|
654
|
-
// ... close modal
|
|
655
|
-
restoreFocusTrap() // cancel focus trap and restore focus to the last active element before enablig the focus trap
|
|
656
|
-
}, [ restoreFocusTrap ] )
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
---
|
|
660
|
-
|
|
661
|
-
###### Defining the target ondemand
|
|
662
|
-
|
|
663
|
-
```tsx
|
|
664
|
-
import { useFocusTrap } from '@alessiofrittoli/react-hooks'
|
|
665
|
-
|
|
666
|
-
const modalRef = useRef<HTMLDivElement>( null )
|
|
667
|
-
const modal2Ref = useRef<HTMLDivElement>( null )
|
|
668
|
-
const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap()
|
|
669
|
-
|
|
670
|
-
const modalOpenHandler = useCallback( () => {
|
|
671
|
-
if ( ! modalRef.current ) return
|
|
672
|
-
// ... open modal
|
|
673
|
-
setFocusTrap( modalRef.current )
|
|
674
|
-
modalRef.current.focus()
|
|
675
|
-
}, [ setFocusTrap ] )
|
|
676
|
-
|
|
677
|
-
const modal2OpenHandler = useCallback( () => {
|
|
678
|
-
if ( ! modal2Ref.current ) return
|
|
679
|
-
// ... open modal
|
|
680
|
-
setFocusTrap( modal2Ref.current )
|
|
681
|
-
modal2Ref.current.focus()
|
|
682
|
-
}, [ setFocusTrap ] )
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
</details>
|
|
686
|
-
|
|
687
|
-
---
|
|
688
|
-
|
|
689
|
-
##### `useInView`
|
|
690
|
-
|
|
691
|
-
Check if the given target Element is intersecting with an ancestor Element or with a top-level document's viewport.
|
|
692
|
-
|
|
693
|
-
<details>
|
|
694
|
-
|
|
695
|
-
<summary style="cursor:pointer">Parameters</summary>
|
|
696
|
-
|
|
697
|
-
| Parameter | Type | Description |
|
|
698
|
-
|-----------|------|-------------|
|
|
699
|
-
| `target` | `React.RefObject<Element\|null>` | The React.RefObject of the target Element to observe. |
|
|
700
|
-
| `options` | `UseInViewOptions` | (Optional) An object defining custom `IntersectionObserver` options. |
|
|
701
|
-
| `options.root` | `Element\|Document\|false\|null` | (Optional) Identifies the `Element` or `Document` whose bounds are treated as the bounding box of the viewport for the Element which is the observer's target. |
|
|
702
|
-
| `options.margin` | `MarginType` | (Optional) A string, formatted similarly to the CSS margin property's value, which contains offsets for one or more sides of the root's bounding box. |
|
|
703
|
-
| `options.amount` | `'all'\|'some'\|number\|number[]` | (Optional) The intersecting target thresholds. |
|
|
704
|
-
| | | Threshold can be set to: |
|
|
705
|
-
| | | - `all` - `1` will be used. |
|
|
706
|
-
| | | - `some` - `0.5` will be used. |
|
|
707
|
-
| | | - `number` |
|
|
708
|
-
| | | - `number[]` |
|
|
709
|
-
| `options.once` | `boolean` | (Optional) By setting this to `true` the observer will be disconnected after the target Element enters the viewport. |
|
|
710
|
-
| `options.initial` | `boolean` | (Optional) Initial value. This value is used while server rendering then will be updated in the client based on target visibility. Default: `false`. |
|
|
711
|
-
| `options.enable` | `boolean` | (Optional) Defines the initial observation activity. Use the returned `setEnabled` to update this state. Default: `true`. |
|
|
712
|
-
| `options.onIntersect` | `OnIntersectStateHandler` | (Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
|
|
713
|
-
| | | This callback is awaited before any state update. |
|
|
714
|
-
| | | If an error is thrown the React State update won't be fired. |
|
|
715
|
-
| | | ⚠️ Wrap your callback with `useCallback` to avoid unnecessary `IntersectionObserver` recreation. |
|
|
716
|
-
| `options.onEnter` | `OnIntersectHandler` | (Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
|
|
717
|
-
| | | This callback is awaited before any state update. |
|
|
718
|
-
| | | If an error is thrown the React State update won't be fired. |
|
|
719
|
-
| | | ⚠️ Wrap your callback with `useCallback` to avoid unnecessary `IntersectionObserver` recreation. |
|
|
720
|
-
| `options.onExit` | `OnIntersectHandler` | (Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
|
|
721
|
-
| | | This callback is awaited before any state update. |
|
|
722
|
-
| | | If an error is thrown the React State update won't be fired. |
|
|
723
|
-
| | | ⚠️ Wrap your callback with `useCallback` to avoid unnecessary `IntersectionObserver` recreation. |
|
|
724
|
-
|
|
725
|
-
</details>
|
|
726
|
-
|
|
727
|
-
---
|
|
728
|
-
|
|
729
|
-
<details>
|
|
730
|
-
|
|
731
|
-
<summary style="cursor:pointer">Returns</summary>
|
|
732
|
-
|
|
733
|
-
Type: `UseInViewReturnType`
|
|
734
|
-
|
|
735
|
-
An object containing:
|
|
736
|
-
|
|
737
|
-
- `inView`: `boolean` - Indicates whether the target Element is in viewport or not.
|
|
738
|
-
- `setInView`: `React.Dispatch<React.SetStateAction<boolean>>` - A React Dispatch SetState action that allows custom state updates.
|
|
739
|
-
- `enabled`: `boolean` - Indicates whether the target Element is being observed or not.
|
|
740
|
-
- `setEnabled`: `React.Dispatch<React.SetStateAction<boolean>>` - A React Dispatch SetState action that allows to enable/disable observation when needed.
|
|
741
|
-
- `observer`: `IntersectionObserver | undefined` - The `IntersectionObserver` instance. It could be `undefined` if `IntersectionObserver` is not available or observation is not enabled.
|
|
742
|
-
|
|
743
|
-
</details>
|
|
744
|
-
|
|
745
|
-
---
|
|
746
|
-
|
|
747
|
-
<details>
|
|
748
|
-
|
|
749
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
750
|
-
|
|
751
|
-
###### Basic usage
|
|
752
|
-
|
|
753
|
-
```tsx
|
|
754
|
-
'use client'
|
|
755
|
-
|
|
756
|
-
import { useRef } from 'react'
|
|
757
|
-
import { useInView } from '@alessiofrittoli/react-hooks'
|
|
758
|
-
|
|
759
|
-
const UseInViewExample: React.FC = () => {
|
|
760
|
-
|
|
761
|
-
const targetRef = useRef<HTMLDivElement>( null )
|
|
762
|
-
const { inView } = useInView( ref )
|
|
763
|
-
|
|
764
|
-
return (
|
|
765
|
-
Array.from( Array( 6 ) ).map( ( value, index ) => (
|
|
766
|
-
<div
|
|
767
|
-
key={ index }
|
|
768
|
-
style={ {
|
|
769
|
-
height : '50vh',
|
|
770
|
-
border : '1px solid red',
|
|
771
|
-
display : 'flex',
|
|
772
|
-
alignItems : 'center',
|
|
773
|
-
justifyContent : 'center',
|
|
774
|
-
} }
|
|
775
|
-
>
|
|
776
|
-
<div
|
|
777
|
-
ref={ index === 2 ? targetRef : undefined }
|
|
778
|
-
style={ {
|
|
779
|
-
width : 150,
|
|
780
|
-
height : 150,
|
|
781
|
-
borderRadius : 12,
|
|
782
|
-
display : 'flex',
|
|
783
|
-
alignItems : 'center',
|
|
784
|
-
justifyContent : 'center',
|
|
785
|
-
background : inView ? '#51AF83' : '#201A1B',
|
|
786
|
-
color : inView ? '#201A1B' : '#FFFFFF',
|
|
787
|
-
} }
|
|
788
|
-
>{ index + 1 }</div>
|
|
789
|
-
</div>
|
|
790
|
-
) )
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
}
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
---
|
|
797
|
-
|
|
798
|
-
###### Disconnect observer after target enters the viewport
|
|
799
|
-
|
|
800
|
-
```tsx
|
|
801
|
-
'use client'
|
|
802
|
-
|
|
803
|
-
import { useRef } from 'react'
|
|
804
|
-
import { useInView } from '@alessiofrittoli/react-hooks'
|
|
805
|
-
|
|
806
|
-
const OnceExample: React.FC = () => {
|
|
807
|
-
|
|
808
|
-
const targetRef = useRef<HTMLDivElement>( null )
|
|
809
|
-
const { inView } = useInView( targetRef, { once: true } )
|
|
810
|
-
|
|
811
|
-
useEffect( () => {
|
|
812
|
-
|
|
813
|
-
if ( ! inView ) return
|
|
814
|
-
console.count( 'Fired only once: element entered viewport.' )
|
|
815
|
-
|
|
816
|
-
}, [ inView ] )
|
|
817
|
-
|
|
818
|
-
return (
|
|
819
|
-
<div
|
|
820
|
-
ref={ targetRef }
|
|
821
|
-
style={ {
|
|
822
|
-
height : 200,
|
|
823
|
-
background : inView ? 'lime' : 'gray',
|
|
824
|
-
} }
|
|
825
|
-
/>
|
|
826
|
-
)
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
---
|
|
832
|
-
|
|
833
|
-
###### Observe target only when needed
|
|
834
|
-
|
|
835
|
-
```tsx
|
|
836
|
-
'use client'
|
|
837
|
-
|
|
838
|
-
import { useRef } from 'react'
|
|
839
|
-
import { useInView } from '@alessiofrittoli/react-hooks'
|
|
840
|
-
|
|
841
|
-
const OnDemandObservation: React.FC = () => {
|
|
842
|
-
|
|
843
|
-
const targetRef = useRef<HTMLDivElement>( null )
|
|
844
|
-
const {
|
|
845
|
-
inView, enabled, setEnabled
|
|
846
|
-
} = useInView( targetRef, { enable: false } )
|
|
847
|
-
|
|
848
|
-
return (
|
|
849
|
-
<div>
|
|
850
|
-
<button onClick={ () => setEnabled( prev => ! prev ) }>
|
|
851
|
-
{ enabled ? 'Disconnect observer' : 'Observe' }
|
|
852
|
-
</button>
|
|
853
|
-
<div
|
|
854
|
-
ref={ targetRef }
|
|
855
|
-
style={ {
|
|
856
|
-
height : 200,
|
|
857
|
-
marginTop : 50,
|
|
858
|
-
background : inView ? 'lime' : 'gray',
|
|
859
|
-
} }
|
|
860
|
-
/>
|
|
861
|
-
</div>
|
|
862
|
-
)
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
---
|
|
868
|
-
|
|
869
|
-
###### Execute custom callback when intersection occurs
|
|
870
|
-
|
|
871
|
-
```tsx
|
|
872
|
-
'use client'
|
|
873
|
-
|
|
874
|
-
import { useRef } from 'react'
|
|
875
|
-
import { useInView, type OnIntersectStateHandler } from '@alessiofrittoli/react-hooks'
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const AsyncStartExample: React.FC = () => {
|
|
879
|
-
|
|
880
|
-
const targetRef = useRef<HTMLDivElement>( null )
|
|
881
|
-
const onIntersect = useCallback<OnIntersectStateHandler>( async ( { entry, isEntering } ) => {
|
|
882
|
-
|
|
883
|
-
if ( isEntering ) {
|
|
884
|
-
console.log( 'Delaying state update...' )
|
|
885
|
-
await new Promise( resolve => setTimeout( resolve, 1000 ) ) // Simulate delay
|
|
886
|
-
console.log( 'Async task completed. `inView` will now be updated.' )
|
|
887
|
-
return
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
console.log( 'Delaying state update...' )
|
|
891
|
-
await new Promise( resolve => setTimeout( resolve, 1000 ) ) // Simulate delay
|
|
892
|
-
console.log( 'Async task completed. `inView` will now be updated.' )
|
|
893
|
-
|
|
894
|
-
}, [] )
|
|
895
|
-
|
|
896
|
-
const { inView } = useInView( targetRef, { onIntersect } )
|
|
897
|
-
|
|
898
|
-
return (
|
|
899
|
-
<div
|
|
900
|
-
ref={ targetRef }
|
|
901
|
-
style={ {
|
|
902
|
-
height : 200,
|
|
903
|
-
background : inView ? 'lime' : 'gray',
|
|
904
|
-
} }
|
|
905
|
-
/>
|
|
906
|
-
)
|
|
907
|
-
}
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
---
|
|
911
|
-
|
|
912
|
-
###### Execute custom callback when `onEnter` and `onExit`
|
|
913
|
-
|
|
914
|
-
```tsx
|
|
915
|
-
'use client'
|
|
916
|
-
|
|
917
|
-
import { useRef } from 'react'
|
|
918
|
-
import { useInView, type OnIntersectHandler } from '@alessiofrittoli/react-hooks'
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const AsyncStartExample: React.FC = () => {
|
|
922
|
-
|
|
923
|
-
const targetRef = useRef<HTMLDivElement>( null )
|
|
924
|
-
const onEnter = useCallback<OnIntersectHandler>( async ( { entry } ) => {
|
|
925
|
-
console.log( 'In viewport - ', entry )
|
|
926
|
-
}, [] )
|
|
927
|
-
const onExit = useCallback<OnIntersectHandler>( async ( { entry } ) => {
|
|
928
|
-
console.log( 'Exited viewport - ', entry )
|
|
929
|
-
}, [] )
|
|
930
|
-
|
|
931
|
-
const { inView } = useInView( targetRef, { onEnter, onExit } )
|
|
932
|
-
|
|
933
|
-
return (
|
|
934
|
-
<div
|
|
935
|
-
ref={ targetRef }
|
|
936
|
-
style={ {
|
|
937
|
-
height : 200,
|
|
938
|
-
background : inView ? 'lime' : 'gray',
|
|
939
|
-
} }
|
|
940
|
-
/>
|
|
941
|
-
)
|
|
942
|
-
}
|
|
943
|
-
```
|
|
944
|
-
|
|
945
|
-
</details>
|
|
946
|
-
|
|
947
|
-
---
|
|
948
|
-
|
|
949
|
-
#### Miscellaneous
|
|
950
|
-
|
|
951
|
-
---
|
|
952
|
-
|
|
953
|
-
##### `useInput`
|
|
954
|
-
|
|
955
|
-
Docs coming soon
|
|
956
|
-
|
|
957
|
-
---
|
|
958
|
-
|
|
959
|
-
##### `useEffectOnce`
|
|
960
|
-
|
|
961
|
-
Docs coming soon
|
|
962
|
-
|
|
963
|
-
---
|
|
964
|
-
|
|
965
|
-
##### `useIsClient`
|
|
966
|
-
|
|
967
|
-
Check if the React Hook or Component where this hook is executed is running in a browser environment.
|
|
968
|
-
|
|
969
|
-
This is pretty usefull to avoid hydration errors.
|
|
970
|
-
|
|
971
|
-
<details>
|
|
972
|
-
|
|
973
|
-
<summary style="cursor:pointer">Returns</summary>
|
|
974
|
-
|
|
975
|
-
Type: `boolean`
|
|
976
|
-
|
|
977
|
-
- `true` if the React Hook or Component is running in a browser environment.
|
|
978
|
-
- `false` otherwise.
|
|
979
|
-
|
|
980
|
-
</details>
|
|
981
|
-
|
|
982
|
-
---
|
|
983
|
-
|
|
984
|
-
<details>
|
|
985
|
-
|
|
986
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
987
|
-
|
|
988
|
-
###### Basic usage
|
|
989
|
-
|
|
990
|
-
```tsx
|
|
991
|
-
'use client'
|
|
992
|
-
|
|
993
|
-
import { useIsClient } from '@alessiofrittoli/react-hooks'
|
|
994
|
-
|
|
995
|
-
export const ClientComponent: React.FC = () => {
|
|
996
|
-
|
|
997
|
-
const isClient = useIsClient()
|
|
998
|
-
|
|
999
|
-
return (
|
|
1000
|
-
<div>Running { ! isClient ? 'server' : 'client' }-side</div>
|
|
1001
|
-
)
|
|
1002
|
-
|
|
1003
|
-
}
|
|
1004
|
-
```
|
|
1005
|
-
|
|
1006
|
-
</details>
|
|
1007
|
-
|
|
1008
|
-
---
|
|
1009
|
-
|
|
1010
|
-
##### `useIsFirstRender`
|
|
1011
|
-
|
|
1012
|
-
Check if is first React Hook/Component render.
|
|
1013
|
-
|
|
1014
|
-
<details>
|
|
1015
|
-
|
|
1016
|
-
<summary style="cursor:pointer">Returns</summary>
|
|
1017
|
-
|
|
1018
|
-
Type: `boolean`
|
|
1019
|
-
|
|
1020
|
-
- `true` at the mount time.
|
|
1021
|
-
- `false` otherwise.
|
|
1022
|
-
|
|
1023
|
-
Note that if the React Hook/Component has no state updates, `useIsFirstRender` will always return `true`.
|
|
1024
|
-
|
|
1025
|
-
</details>
|
|
1026
|
-
|
|
1027
|
-
---
|
|
1028
|
-
|
|
1029
|
-
<details>
|
|
1030
|
-
|
|
1031
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
1032
|
-
|
|
1033
|
-
###### Basic usage
|
|
1034
|
-
|
|
1035
|
-
```tsx
|
|
1036
|
-
'use client'
|
|
1037
|
-
|
|
1038
|
-
import { useIsFirstRender } from '@alessiofrittoli/react-hooks'
|
|
1039
|
-
|
|
1040
|
-
export const ClientComponent: React.FC = () => {
|
|
1041
|
-
|
|
1042
|
-
const isFirstRender = useIsFirstRender()
|
|
1043
|
-
const [ counter, setCounter ] = useState( 0 )
|
|
1044
|
-
|
|
1045
|
-
useEffect( () => {
|
|
1046
|
-
const intv = setInterval( () => {
|
|
1047
|
-
setCounter( prev => prev + 1 )
|
|
1048
|
-
}, 1000 )
|
|
1049
|
-
return () => clearInterval( intv )
|
|
1050
|
-
}, [] )
|
|
1051
|
-
|
|
1052
|
-
return (
|
|
1053
|
-
<div>
|
|
1054
|
-
{ isFirstRender ? 'First render' : 'Subsequent render' }
|
|
1055
|
-
<hr />
|
|
1056
|
-
{ counter }
|
|
1057
|
-
</div>
|
|
1058
|
-
)
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
</details>
|
|
1064
|
-
|
|
1065
|
-
---
|
|
1066
|
-
|
|
1067
|
-
##### `useUpdateEffect`
|
|
1068
|
-
|
|
1069
|
-
Modified version of `useEffect` that skips the first render.
|
|
1070
|
-
|
|
1071
|
-
<details>
|
|
1072
|
-
|
|
1073
|
-
<summary style="cursor:pointer">Parameters</summary>
|
|
1074
|
-
|
|
1075
|
-
| Parameter | Type | Description |
|
|
1076
|
-
|-----------|------------------------|-------------|
|
|
1077
|
-
| `effect` | `React.EffectCallback` | Imperative function that can return a cleanup function. |
|
|
1078
|
-
| `deps` | `React.DependencyList` | If present, effect will only activate if the values in the list change. |
|
|
1079
|
-
|
|
1080
|
-
</details>
|
|
1081
|
-
|
|
1082
|
-
---
|
|
1083
|
-
|
|
1084
|
-
<details>
|
|
1085
|
-
|
|
1086
|
-
<summary style="cursor:pointer">Usage</summary>
|
|
1087
|
-
|
|
1088
|
-
###### Basic usage
|
|
1089
|
-
|
|
1090
|
-
```tsx
|
|
1091
|
-
'use client'
|
|
1092
|
-
|
|
1093
|
-
import { useEffect, useState } from 'react'
|
|
1094
|
-
import { useUpdateEffect } from '@alessiofrittoli/react-hooks'
|
|
1095
|
-
|
|
1096
|
-
export const ClientComponent: React.FC = () => {
|
|
1097
|
-
|
|
1098
|
-
const [ count, setCount ] = useState( 0 )
|
|
1099
|
-
|
|
1100
|
-
useEffect( () => {
|
|
1101
|
-
const intv = setInterval( () => {
|
|
1102
|
-
setCount( prev => prev + 1 )
|
|
1103
|
-
}, 1000 )
|
|
1104
|
-
return () => clearInterval( intv )
|
|
1105
|
-
}, [] )
|
|
1106
|
-
|
|
1107
|
-
useEffect( () => {
|
|
1108
|
-
console.log( 'useEffect', count ) // starts from 0
|
|
1109
|
-
return () => {
|
|
1110
|
-
console.log( 'useEffect - clean up', count ) // starts from 0
|
|
1111
|
-
}
|
|
1112
|
-
}, [ count ] )
|
|
1113
|
-
|
|
1114
|
-
useUpdateEffect( () => {
|
|
1115
|
-
console.log( 'useUpdateEffect', count ) // starts from 1
|
|
1116
|
-
return () => {
|
|
1117
|
-
console.log( 'useUpdateEffect - clean up', count ) // starts from 1
|
|
1118
|
-
}
|
|
1119
|
-
}, [ count ] )
|
|
1120
|
-
|
|
1121
|
-
return (
|
|
1122
|
-
<div>{ count }</div>
|
|
1123
|
-
)
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
```
|
|
1127
|
-
|
|
1128
|
-
</details>
|
|
1129
|
-
|
|
1130
|
-
---
|
|
1131
|
-
|
|
1132
|
-
##### `usePagination`
|
|
1133
|
-
|
|
1134
|
-
Get pagination informations based on the given options.
|
|
1135
|
-
|
|
1136
|
-
This hook memoize the returned result of the [`paginate`](https://github.com/alessiofrittoli/math-utils/blob/master/docs/helpers/README.md#paginate) function imported from [`@alessiofrittoli/math-utils`](https://npmjs.com/package/@alessiofrittoli/math-utils).
|
|
1137
|
-
|
|
1138
|
-
See [`paginate`](https://github.com/alessiofrittoli/math-utils/blob/master/docs/helpers/README.md#paginate) function Documentation for more information about it.
|
|
1139
|
-
|
|
1140
|
-
---
|
|
1141
|
-
|
|
1142
|
-
##### `useSelection`
|
|
1143
|
-
|
|
1144
|
-
Docs coming soon
|
|
1145
|
-
|
|
1146
|
-
---
|
|
1147
|
-
|
|
1148
|
-
### Development
|
|
1149
|
-
|
|
1150
|
-
#### Install depenendencies
|
|
1151
|
-
|
|
1152
|
-
```bash
|
|
1153
|
-
npm install
|
|
1154
|
-
```
|
|
1155
|
-
|
|
1156
|
-
or using `pnpm`
|
|
1157
|
-
|
|
1158
|
-
```bash
|
|
1159
|
-
pnpm i
|
|
1160
|
-
```
|
|
1161
|
-
|
|
1162
|
-
#### Build the source code
|
|
1163
|
-
|
|
1164
|
-
Run the following command to test and build code for distribution.
|
|
1165
|
-
|
|
1166
|
-
```bash
|
|
1167
|
-
pnpm build
|
|
1168
|
-
```
|
|
1169
|
-
|
|
1170
|
-
#### [ESLint](https://www.npmjs.com/package/eslint)
|
|
1171
|
-
|
|
1172
|
-
warnings / errors check.
|
|
1173
|
-
|
|
1174
|
-
```bash
|
|
1175
|
-
pnpm lint
|
|
1176
|
-
```
|
|
1177
|
-
|
|
1178
|
-
#### [Jest](https://npmjs.com/package/jest)
|
|
1179
|
-
|
|
1180
|
-
Run all the defined test suites by running the following:
|
|
1181
|
-
|
|
1182
|
-
```bash
|
|
1183
|
-
# Run tests and watch file changes.
|
|
1184
|
-
pnpm test:watch
|
|
1185
|
-
|
|
1186
|
-
# Run tests in a CI environment.
|
|
1187
|
-
pnpm test:ci
|
|
1188
|
-
```
|
|
1189
|
-
|
|
1190
|
-
- See [`package.json`](./package.json) file scripts for more info.
|
|
1191
|
-
|
|
1192
|
-
Run tests with coverage.
|
|
1193
|
-
|
|
1194
|
-
An HTTP server is then started to serve coverage files from `./coverage` folder.
|
|
1195
|
-
|
|
1196
|
-
⚠️ You may see a blank page the first time you run this command. Simply refresh the browser to see the updates.
|
|
1197
|
-
|
|
1198
|
-
```bash
|
|
1199
|
-
test:coverage:serve
|
|
1200
|
-
```
|
|
1201
|
-
|
|
1202
|
-
---
|
|
1203
|
-
|
|
1204
|
-
### Contributing
|
|
1205
|
-
|
|
1206
|
-
Contributions are truly welcome!
|
|
1207
|
-
|
|
1208
|
-
Please refer to the [Contributing Doc](./CONTRIBUTING.md) for more information on how to start contributing to this project.
|
|
1209
|
-
|
|
1210
|
-
Help keep this project up to date with [GitHub Sponsor][sponsor-url].
|
|
1211
|
-
|
|
1212
|
-
[![GitHub Sponsor][sponsor-badge]][sponsor-url]
|
|
1213
|
-
|
|
1214
|
-
---
|
|
1215
|
-
|
|
1216
|
-
### Security
|
|
1217
|
-
|
|
1218
|
-
If you believe you have found a security vulnerability, we encourage you to **_responsibly disclose this and NOT open a public issue_**. We will investigate all legitimate reports. Email `security@alessiofrittoli.it` to disclose any security vulnerabilities.
|
|
1219
|
-
|
|
1220
|
-
### Made with ☕
|
|
1221
|
-
|
|
1222
|
-
<table style='display:flex;gap:20px;'>
|
|
1223
|
-
<tbody>
|
|
1224
|
-
<tr>
|
|
1225
|
-
<td>
|
|
1226
|
-
<img alt="avatar" src='https://avatars.githubusercontent.com/u/35973186' style='width:60px;border-radius:50%;object-fit:contain;'>
|
|
1227
|
-
</td>
|
|
1228
|
-
<td>
|
|
1229
|
-
<table style='display:flex;gap:2px;flex-direction:column;'>
|
|
1230
|
-
<tbody>
|
|
1231
|
-
<tr>
|
|
1232
|
-
<td>
|
|
1233
|
-
<a href='https://github.com/alessiofrittoli' target='_blank' rel='noopener'>Alessio Frittoli</a>
|
|
1234
|
-
</td>
|
|
1235
|
-
</tr>
|
|
1236
|
-
<tr>
|
|
1237
|
-
<td>
|
|
1238
|
-
<small>
|
|
1239
|
-
<a href='https://alessiofrittoli.it' target='_blank' rel='noopener'>https://alessiofrittoli.it</a> |
|
|
1240
|
-
<a href='mailto:info@alessiofrittoli.it' target='_blank' rel='noopener'>info@alessiofrittoli.it</a>
|
|
1241
|
-
</small>
|
|
1242
|
-
</td>
|
|
1243
|
-
</tr>
|
|
1244
|
-
</tbody>
|
|
1245
|
-
</table>
|
|
1246
|
-
</td>
|
|
1247
|
-
</tr>
|
|
1248
|
-
</tbody>
|
|
1249
|
-
</table>
|
|
1
|
+
# React Hooks 🪝
|
|
2
|
+
|
|
3
|
+
[![NPM Latest Version][version-badge]][npm-url] [![Coverage Status][coverage-badge]][coverage-url] [![Socket Status][socket-badge]][socket-url] [![NPM Monthly Downloads][downloads-badge]][npm-url] [![Dependencies][deps-badge]][deps-url]
|
|
4
|
+
|
|
5
|
+
[![GitHub Sponsor][sponsor-badge]][sponsor-url]
|
|
6
|
+
|
|
7
|
+
[version-badge]: https://img.shields.io/npm/v/%40alessiofrittoli%2Freact-hooks
|
|
8
|
+
[npm-url]: https://npmjs.org/package/%40alessiofrittoli%2Freact-hooks
|
|
9
|
+
[coverage-badge]: https://coveralls.io/repos/github/alessiofrittoli/react-hooks/badge.svg
|
|
10
|
+
[coverage-url]: https://coveralls.io/github/alessiofrittoli/react-hooks
|
|
11
|
+
[socket-badge]: https://socket.dev/api/badge/npm/package/@alessiofrittoli/react-hooks
|
|
12
|
+
[socket-url]: https://socket.dev/npm/package/@alessiofrittoli/react-hooks/overview
|
|
13
|
+
[downloads-badge]: https://img.shields.io/npm/dm/%40alessiofrittoli%2Freact-hooks.svg
|
|
14
|
+
[deps-badge]: https://img.shields.io/librariesio/release/npm/%40alessiofrittoli%2Freact-hooks
|
|
15
|
+
[deps-url]: https://libraries.io/npm/%40alessiofrittoli%2Freact-hooks
|
|
16
|
+
|
|
17
|
+
[sponsor-badge]: https://img.shields.io/static/v1?label=Fund%20this%20package&message=%E2%9D%A4&logo=GitHub&color=%23DB61A2
|
|
18
|
+
[sponsor-url]: https://github.com/sponsors/alessiofrittoli
|
|
19
|
+
|
|
20
|
+
## TypeScript React utility Hooks
|
|
21
|
+
|
|
22
|
+
### Table of Contents
|
|
23
|
+
|
|
24
|
+
- [Getting started](#getting-started)
|
|
25
|
+
- [ESLint Configuration](#eslint-configuration)
|
|
26
|
+
- [API Reference](#api-reference)
|
|
27
|
+
- [Browser API](#browser-api)
|
|
28
|
+
- [`useStorage`](#usestorage)
|
|
29
|
+
- [`useLocalStorage`](#uselocalstorage)
|
|
30
|
+
- [`useSessionStorage`](#usesessionstorage)
|
|
31
|
+
- [`useMediaQuery`](#usemediaquery)
|
|
32
|
+
- [`useDarkMode`](#usedarkmode)
|
|
33
|
+
- [`useIsPortrait`](#useisportrait)
|
|
34
|
+
- [DOM API](#dom-api)
|
|
35
|
+
- [`useScrollBlock`](#usescrollblock)
|
|
36
|
+
- [`useFocusTrap`](#usefocustrap)
|
|
37
|
+
- [`useInView`](#useinview)
|
|
38
|
+
- [Miscellaneous](#miscellaneous)
|
|
39
|
+
- [`useIsClient`](#useisclient)
|
|
40
|
+
- [`useIsFirstRender`](#useisfirstrender)
|
|
41
|
+
- [`useUpdateEffect`](#useupdateeffect)
|
|
42
|
+
- [`usePagination`](#usepagination)
|
|
43
|
+
- [Development](#development)
|
|
44
|
+
- [Install depenendencies](#install-depenendencies)
|
|
45
|
+
- [Build the source code](#build-the-source-code)
|
|
46
|
+
- [ESLint](#eslint)
|
|
47
|
+
- [Jest](#jest)
|
|
48
|
+
- [Contributing](#contributing)
|
|
49
|
+
- [Security](#security)
|
|
50
|
+
- [Credits](#made-with-)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Getting started
|
|
55
|
+
|
|
56
|
+
Run the following command to start using `react-hooks` in your projects:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm i @alessiofrittoli/react-hooks
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
or using `pnpm`
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm i @alessiofrittoli/react-hooks
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### ESLint Configuration
|
|
71
|
+
|
|
72
|
+
This library may define and exports hooks that requires additional ESLint configuration for your project such as [`useUpdateEffect`](#useupdateeffect).
|
|
73
|
+
|
|
74
|
+
Simply imports recommended configuration from `@alessiofrittoli/react-hooks/eslint` and add them to your ESLint configuration like so:
|
|
75
|
+
|
|
76
|
+
```mjs
|
|
77
|
+
import { config as AFReactHooksEslint } from '@alessiofrittoli/react-hooks/eslint'
|
|
78
|
+
|
|
79
|
+
/** @type {import('eslint').Linter.Config[]} */
|
|
80
|
+
const config = [
|
|
81
|
+
...AFReactHooksEslint.recommended,
|
|
82
|
+
// ... other configurations
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
export default config
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### API Reference
|
|
92
|
+
|
|
93
|
+
#### Browser API
|
|
94
|
+
|
|
95
|
+
##### Storage
|
|
96
|
+
|
|
97
|
+
The following storage hooks use Storage Utilities from [`@alessiofrittoli/web-utils`](https://npmjs.com/package/@alessiofrittoli/web-utils#storage-utilities) adding a React oriented implementation.
|
|
98
|
+
|
|
99
|
+
###### `useStorage`
|
|
100
|
+
|
|
101
|
+
Easly handle Local or Session Storage State.
|
|
102
|
+
|
|
103
|
+
<details>
|
|
104
|
+
|
|
105
|
+
<summary style="cursor:pointer">Type parameters</summary>
|
|
106
|
+
|
|
107
|
+
| Parameter | Type | Default | Description |
|
|
108
|
+
|-----------|------|---------|-------------|
|
|
109
|
+
| `T` | `any` | `string` | A custom type applied to the stored item. |
|
|
110
|
+
|
|
111
|
+
</details>
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
<details>
|
|
116
|
+
|
|
117
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
118
|
+
|
|
119
|
+
| Parameter | Type | Default | Description |
|
|
120
|
+
|-----------|------|---------|-------------|
|
|
121
|
+
| `key` | `string` | - | The storage item key. |
|
|
122
|
+
| `initial` | `T` | - | The storage item initial value. |
|
|
123
|
+
| `type` | `local\|session` | local | (Optional) The storage API to use. |
|
|
124
|
+
|
|
125
|
+
</details>
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
<details>
|
|
130
|
+
|
|
131
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
132
|
+
|
|
133
|
+
Type: `[ Value<T>, SetValue<Value<T>> ]`
|
|
134
|
+
|
|
135
|
+
A tuple with the stored item value or initial value and the setter function.
|
|
136
|
+
|
|
137
|
+
</details>
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
|
|
143
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
144
|
+
|
|
145
|
+
###### Importing the hooks
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import {
|
|
149
|
+
useStorage, useLocalStorage, useSessionStorage
|
|
150
|
+
} from '@alessiofrittoli/react-hooks'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
###### Reading item value from storage
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
'use client'
|
|
159
|
+
|
|
160
|
+
import { useStorage } from '@alessiofrittoli/react-hooks'
|
|
161
|
+
|
|
162
|
+
type Locale = 'it' | 'en'
|
|
163
|
+
|
|
164
|
+
const storage = 'local' // or 'session'
|
|
165
|
+
const defaultLocale = 'it'
|
|
166
|
+
|
|
167
|
+
export const SomeComponent: React.FC = () => {
|
|
168
|
+
|
|
169
|
+
const [ userLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
...
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
###### Updating storage item value
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
'use client'
|
|
184
|
+
|
|
185
|
+
import { useCallback } from 'react'
|
|
186
|
+
import { useStorage } from '@alessiofrittoli/react-hooks'
|
|
187
|
+
|
|
188
|
+
type Locale = 'it' | 'en'
|
|
189
|
+
|
|
190
|
+
const storage = 'local' // or 'session'
|
|
191
|
+
const defaultLocale = 'it'
|
|
192
|
+
|
|
193
|
+
export const LanguageSwitcher: React.FC = () => {
|
|
194
|
+
|
|
195
|
+
const [ userLocale, setUserLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
|
|
196
|
+
|
|
197
|
+
const clickHandler = useCallback( () => {
|
|
198
|
+
setUserLocale( 'en' )
|
|
199
|
+
}, [ setUserLocale ] )
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
...
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
###### Deleting storage item
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
'use client'
|
|
214
|
+
|
|
215
|
+
import { useCallback } from 'react'
|
|
216
|
+
import { useStorage } from '@alessiofrittoli/react-hooks'
|
|
217
|
+
|
|
218
|
+
type Locale = 'it' | 'en'
|
|
219
|
+
|
|
220
|
+
const storage = 'local' // or 'session'
|
|
221
|
+
const defaultLocale = 'it'
|
|
222
|
+
|
|
223
|
+
export const LanguageSwitcher: React.FC = () => {
|
|
224
|
+
|
|
225
|
+
const [ userLocale, setUserLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
|
|
226
|
+
|
|
227
|
+
const deleteHandler = useCallback( () => {
|
|
228
|
+
setUserLocale( null )
|
|
229
|
+
// or
|
|
230
|
+
setUserLocale( undefined )
|
|
231
|
+
// or
|
|
232
|
+
setUserLocale( '' )
|
|
233
|
+
}, [ setUserLocale ] )
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
...
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
</details>
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
###### `useLocalStorage`
|
|
247
|
+
|
|
248
|
+
Shortcut React Hook for [`useStorage`](#usestorage).
|
|
249
|
+
|
|
250
|
+
Applies the same API Reference.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
###### `useSessionStorage`
|
|
255
|
+
|
|
256
|
+
Shortcut React Hook for [`useStorage`](#usestorage).
|
|
257
|
+
|
|
258
|
+
Applies the same API Reference.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
##### `useMediaQuery`
|
|
263
|
+
|
|
264
|
+
Get Document Media matches and listen for changes.
|
|
265
|
+
|
|
266
|
+
<details>
|
|
267
|
+
|
|
268
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
269
|
+
|
|
270
|
+
| Parameter | Type | Default | Description |
|
|
271
|
+
|-----------|----------|---------|-------------|
|
|
272
|
+
| `query` | `string` | - | A string specifying the media query to parse into a `MediaQueryList`. |
|
|
273
|
+
| `options` | `UseMediaQueryOptions\|UseMediaQueryStateOptions` | - | An object defining custom options. |
|
|
274
|
+
| `options.updateState` | `boolean` | `true` | Indicates whether the hook will dispatch a React state update when the given `query` change event get dispatched. |
|
|
275
|
+
| `options.onChange` | `OnChangeHandler` | - | A custom callback that will be invoked on initial page load and when the given `query` change event get dispatched. |
|
|
276
|
+
| | | | This callback is required if `updateState` is set to `false`. |
|
|
277
|
+
|
|
278
|
+
</details>
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
<details>
|
|
283
|
+
|
|
284
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
285
|
+
|
|
286
|
+
Type: `boolean|void`
|
|
287
|
+
|
|
288
|
+
- `true` or `false` if the document currently matches the media query list or not.
|
|
289
|
+
- `void` if `updateState` is set to `false`.
|
|
290
|
+
|
|
291
|
+
</details>
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
<details>
|
|
296
|
+
|
|
297
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
298
|
+
|
|
299
|
+
###### Check if user device prefers dark color scheme
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import { useMediaQuery } from '@alessiofrittoli/react-hooks'
|
|
303
|
+
|
|
304
|
+
const isDarkOS = useMediaQuery( '(prefers-color-scheme: dark)' )
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
###### Listen changes with no state updates
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
import { useMediaQuery } from '@alessiofrittoli/react-hooks'
|
|
313
|
+
|
|
314
|
+
useMediaQuery( '(prefers-color-scheme: dark)', {
|
|
315
|
+
updateState: false,
|
|
316
|
+
onChange( matches ) {
|
|
317
|
+
console.log( 'is dark OS?', matches )
|
|
318
|
+
}
|
|
319
|
+
} )
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
</details>
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
##### `useConnection`
|
|
327
|
+
|
|
328
|
+
Docs coming soon
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
##### `useDarkMode`
|
|
333
|
+
|
|
334
|
+
Easily manage dark mode with full respect for user device preferences.
|
|
335
|
+
|
|
336
|
+
This hook is user-oriented and built to honor system-level color scheme settings:
|
|
337
|
+
|
|
338
|
+
- If the device prefers a dark color scheme, dark mode is automatically enabled on first load.
|
|
339
|
+
- If the user enables/disables dark mode via a web widget, the preference is stored in `localStorage` under the key `dark-mode`.
|
|
340
|
+
- If the device color scheme preference changes (e.g. via OS settings), that change takes precedence and is stored for future visits.
|
|
341
|
+
|
|
342
|
+
<details>
|
|
343
|
+
|
|
344
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
345
|
+
|
|
346
|
+
| Parameter | Type | Description |
|
|
347
|
+
|-----------|------|-------------|
|
|
348
|
+
| `options` | `UseDarkModeOptions` | (Optional) Configuration object for the hook. |
|
|
349
|
+
| `options.initial` | `boolean` | (Optional) The fallback value to use if no preference is saved in `localStorage`. Defaults to `true` if the device prefers dark mode. |
|
|
350
|
+
| `options.docClassNames` | `[dark: string, light: string]` | (Optional) Array of class names to toggle on the `<html>` element, e.g. `['dark', 'light']`. |
|
|
351
|
+
|
|
352
|
+
</details>
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
<details>
|
|
357
|
+
|
|
358
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
359
|
+
|
|
360
|
+
Type: `UseDarkModeOutput`
|
|
361
|
+
|
|
362
|
+
An object containing utilities for managing dark mode:
|
|
363
|
+
|
|
364
|
+
- `isDarkMode`: `boolean` — Whether dark mode is currently enabled.
|
|
365
|
+
- `isDarkOS`: `boolean` — Whether the user's system prefers dark mode.
|
|
366
|
+
- `toggleDarkMode`: `() => void` — Toggles dark mode and saves the preference.
|
|
367
|
+
- `enableDarkMode`: `() => void` — Enables dark mode and saves the preference.
|
|
368
|
+
- `disableDarkMode`: `() => void` — Disables dark mode and saves the preference.
|
|
369
|
+
|
|
370
|
+
</details>
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
<details>
|
|
375
|
+
|
|
376
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
377
|
+
|
|
378
|
+
###### Basic usage
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
'use client'
|
|
382
|
+
|
|
383
|
+
import { useDarkMode } from '@alessiofrittoli/react-hooks'
|
|
384
|
+
|
|
385
|
+
export const Component: React.FC = () => {
|
|
386
|
+
const { isDarkMode } = useDarkMode()
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<div>{ isDarkMode ? 'Dark mode enabled' : 'Dark mode disabled' }</div>
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
###### Update Document class names for CSS styling
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
// Component.tsx
|
|
400
|
+
'use client'
|
|
401
|
+
|
|
402
|
+
import { useDarkMode } from '@alessiofrittoli/react-hooks'
|
|
403
|
+
|
|
404
|
+
export const Component: React.FC = () => {
|
|
405
|
+
const { isDarkMode } = useDarkMode( {
|
|
406
|
+
docClassNames: [ 'dark', 'light' ],
|
|
407
|
+
} )
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<div>{ isDarkMode ? 'Dark mode enabled' : 'Dark mode disabled' }</div>
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
```css
|
|
416
|
+
/* style.css */
|
|
417
|
+
.light {
|
|
418
|
+
color-scheme: light;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.dark {
|
|
422
|
+
color-scheme: dark;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.light body
|
|
426
|
+
{
|
|
427
|
+
color : black;
|
|
428
|
+
background: white;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.dark body
|
|
432
|
+
{
|
|
433
|
+
color : white;
|
|
434
|
+
background: black;
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
###### Custom theme switcher
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
'use client'
|
|
444
|
+
|
|
445
|
+
import { useDarkMode } from '@alessiofrittoli/react-hooks'
|
|
446
|
+
|
|
447
|
+
export const ThemeSwitcher: React.FC = () => {
|
|
448
|
+
const { isDarkMode, toggleDarkMode } = useDarkMode()
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<button onClick={ toggleDarkMode }>
|
|
452
|
+
{ isDarkMode ? '🌙' : '☀️' }
|
|
453
|
+
</button>
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
###### Sync Document theme-color for consistent browser styling
|
|
461
|
+
|
|
462
|
+
Browsers automatically apply colorization using:
|
|
463
|
+
|
|
464
|
+
```html
|
|
465
|
+
<meta name='theme-color' media='(prefers-color-scheme: dark)' />
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
This works based on the OS preference — *not your site theme*. That can cause mismatches if, for example, the system is in dark mode but the user disabled dark mode via a web toggle.
|
|
469
|
+
|
|
470
|
+
To ensure consistency, `useDarkMode` updates these meta tags dynamically based on the actual mode.
|
|
471
|
+
|
|
472
|
+
Just make sure to define both `light` and `dark` theme-color tags in your document:
|
|
473
|
+
|
|
474
|
+
```html
|
|
475
|
+
<head>
|
|
476
|
+
<meta name='theme-color' media='(prefers-color-scheme: light)' content='lime'>
|
|
477
|
+
<meta name='theme-color' media='(prefers-color-scheme: dark)' content='aqua'>
|
|
478
|
+
</head>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
</details>
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
##### `useIsPortrait`
|
|
486
|
+
|
|
487
|
+
Check if device is portrait oriented.
|
|
488
|
+
|
|
489
|
+
React State get updated when device orientation changes.
|
|
490
|
+
|
|
491
|
+
<details>
|
|
492
|
+
|
|
493
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
494
|
+
|
|
495
|
+
Type: `boolean`
|
|
496
|
+
|
|
497
|
+
- `true` if the device is portrait oriented.
|
|
498
|
+
- `false` otherwise.
|
|
499
|
+
|
|
500
|
+
</details>
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
<details>
|
|
505
|
+
|
|
506
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
507
|
+
|
|
508
|
+
###### Check if user device is in landscape
|
|
509
|
+
|
|
510
|
+
```tsx
|
|
511
|
+
import { useIsPortrait } from '@alessiofrittoli/react-hooks'
|
|
512
|
+
|
|
513
|
+
const isLandscape = ! useIsPortrait()
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
</details>
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
#### DOM API
|
|
521
|
+
|
|
522
|
+
##### `useScrollBlock`
|
|
523
|
+
|
|
524
|
+
Prevent Element overflow.
|
|
525
|
+
|
|
526
|
+
<details>
|
|
527
|
+
|
|
528
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
529
|
+
|
|
530
|
+
| Parameter | Type | Default | Description |
|
|
531
|
+
|-----------|------|---------|-------------|
|
|
532
|
+
| `target` | `React.RefObject<HTMLElement\|null>` | `Document.documentElement` | (Optional) The React RefObject target HTMLElement. |
|
|
533
|
+
|
|
534
|
+
</details>
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
<details>
|
|
539
|
+
|
|
540
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
541
|
+
|
|
542
|
+
Type: `[ () => void, () => void ]`
|
|
543
|
+
|
|
544
|
+
A tuple with block and restore scroll callbacks.
|
|
545
|
+
|
|
546
|
+
</details>
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
<details>
|
|
551
|
+
|
|
552
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
553
|
+
|
|
554
|
+
###### Block Document Overflow
|
|
555
|
+
|
|
556
|
+
```tsx
|
|
557
|
+
import { useScrollBlock } from '@alessiofrittoli/react-hooks'
|
|
558
|
+
|
|
559
|
+
const [ blockScroll, restoreScroll ] = useScrollBlock()
|
|
560
|
+
|
|
561
|
+
const openPopUpHandler = useCallback( () => {
|
|
562
|
+
...
|
|
563
|
+
blockScroll()
|
|
564
|
+
}, [ blockScroll ] )
|
|
565
|
+
|
|
566
|
+
const closePopUpHandler = useCallback( () => {
|
|
567
|
+
...
|
|
568
|
+
restoreScroll()
|
|
569
|
+
}, [ restoreScroll ] )
|
|
570
|
+
|
|
571
|
+
...
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
###### Block HTML Element Overflow
|
|
577
|
+
|
|
578
|
+
```tsx
|
|
579
|
+
const elementRef = useRef<HTMLDivElement>( null )
|
|
580
|
+
|
|
581
|
+
const [ blockScroll, restoreScroll ] = useScrollBlock( elementRef )
|
|
582
|
+
|
|
583
|
+
const scrollBlockHandler = useCallback( () => {
|
|
584
|
+
...
|
|
585
|
+
blockScroll()
|
|
586
|
+
}, [ blockScroll ] )
|
|
587
|
+
|
|
588
|
+
const scrollRestoreHandler = useCallback( () => {
|
|
589
|
+
...
|
|
590
|
+
restoreScroll()
|
|
591
|
+
}, [ restoreScroll ] )
|
|
592
|
+
|
|
593
|
+
...
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
</details>
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
##### `useFocusTrap`
|
|
601
|
+
|
|
602
|
+
Trap focus inside the given HTML Element.
|
|
603
|
+
|
|
604
|
+
This comes pretty handy when rendering a modal that shouldn't be closed without a user required action.
|
|
605
|
+
|
|
606
|
+
<details>
|
|
607
|
+
|
|
608
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
609
|
+
|
|
610
|
+
| Parameter | Type | Description |
|
|
611
|
+
|-----------|------|-------------|
|
|
612
|
+
| `target` | `React.RefObject<HTMLElement\|null>` | The target HTMLElement React RefObject to trap focus within. |
|
|
613
|
+
| | | If no target is given, you must provide the target HTMLElement when calling `setFocusTrap`. |
|
|
614
|
+
|
|
615
|
+
</details>
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
<details>
|
|
620
|
+
|
|
621
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
622
|
+
|
|
623
|
+
Type: `readonly [ SetFocusTrap, RestoreFocusTrap ]`
|
|
624
|
+
|
|
625
|
+
A tuple containing:
|
|
626
|
+
|
|
627
|
+
- `setFocusTrap`: A function to enable the focus trap. Optionally accept an HTMLElement as target.
|
|
628
|
+
- `restoreFocusTrap`: A function to restore the previous focus state.
|
|
629
|
+
|
|
630
|
+
</details>
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
<details>
|
|
635
|
+
|
|
636
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
637
|
+
|
|
638
|
+
###### Defining the target on hook initialization
|
|
639
|
+
|
|
640
|
+
```tsx
|
|
641
|
+
import { useFocusTrap } from '@alessiofrittoli/react-hooks'
|
|
642
|
+
|
|
643
|
+
const modalRef = useRef<HTMLDivElement>( null )
|
|
644
|
+
const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap( modalRef )
|
|
645
|
+
|
|
646
|
+
const modalOpenHandler = useCallback( () => {
|
|
647
|
+
if ( ! modalRef.current ) return
|
|
648
|
+
// ... open modal
|
|
649
|
+
setFocusTrap()
|
|
650
|
+
modalRef.current.focus() // focus the dialog so next tab will focus the next element inside the modal
|
|
651
|
+
}, [ setFocusTrap ] )
|
|
652
|
+
|
|
653
|
+
const modalCloseHandler = useCallback( () => {
|
|
654
|
+
// ... close modal
|
|
655
|
+
restoreFocusTrap() // cancel focus trap and restore focus to the last active element before enablig the focus trap
|
|
656
|
+
}, [ restoreFocusTrap ] )
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
###### Defining the target ondemand
|
|
662
|
+
|
|
663
|
+
```tsx
|
|
664
|
+
import { useFocusTrap } from '@alessiofrittoli/react-hooks'
|
|
665
|
+
|
|
666
|
+
const modalRef = useRef<HTMLDivElement>( null )
|
|
667
|
+
const modal2Ref = useRef<HTMLDivElement>( null )
|
|
668
|
+
const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap()
|
|
669
|
+
|
|
670
|
+
const modalOpenHandler = useCallback( () => {
|
|
671
|
+
if ( ! modalRef.current ) return
|
|
672
|
+
// ... open modal
|
|
673
|
+
setFocusTrap( modalRef.current )
|
|
674
|
+
modalRef.current.focus()
|
|
675
|
+
}, [ setFocusTrap ] )
|
|
676
|
+
|
|
677
|
+
const modal2OpenHandler = useCallback( () => {
|
|
678
|
+
if ( ! modal2Ref.current ) return
|
|
679
|
+
// ... open modal
|
|
680
|
+
setFocusTrap( modal2Ref.current )
|
|
681
|
+
modal2Ref.current.focus()
|
|
682
|
+
}, [ setFocusTrap ] )
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
</details>
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
##### `useInView`
|
|
690
|
+
|
|
691
|
+
Check if the given target Element is intersecting with an ancestor Element or with a top-level document's viewport.
|
|
692
|
+
|
|
693
|
+
<details>
|
|
694
|
+
|
|
695
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
696
|
+
|
|
697
|
+
| Parameter | Type | Description |
|
|
698
|
+
|-----------|------|-------------|
|
|
699
|
+
| `target` | `React.RefObject<Element\|null>` | The React.RefObject of the target Element to observe. |
|
|
700
|
+
| `options` | `UseInViewOptions` | (Optional) An object defining custom `IntersectionObserver` options. |
|
|
701
|
+
| `options.root` | `Element\|Document\|false\|null` | (Optional) Identifies the `Element` or `Document` whose bounds are treated as the bounding box of the viewport for the Element which is the observer's target. |
|
|
702
|
+
| `options.margin` | `MarginType` | (Optional) A string, formatted similarly to the CSS margin property's value, which contains offsets for one or more sides of the root's bounding box. |
|
|
703
|
+
| `options.amount` | `'all'\|'some'\|number\|number[]` | (Optional) The intersecting target thresholds. |
|
|
704
|
+
| | | Threshold can be set to: |
|
|
705
|
+
| | | - `all` - `1` will be used. |
|
|
706
|
+
| | | - `some` - `0.5` will be used. |
|
|
707
|
+
| | | - `number` |
|
|
708
|
+
| | | - `number[]` |
|
|
709
|
+
| `options.once` | `boolean` | (Optional) By setting this to `true` the observer will be disconnected after the target Element enters the viewport. |
|
|
710
|
+
| `options.initial` | `boolean` | (Optional) Initial value. This value is used while server rendering then will be updated in the client based on target visibility. Default: `false`. |
|
|
711
|
+
| `options.enable` | `boolean` | (Optional) Defines the initial observation activity. Use the returned `setEnabled` to update this state. Default: `true`. |
|
|
712
|
+
| `options.onIntersect` | `OnIntersectStateHandler` | (Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
|
|
713
|
+
| | | This callback is awaited before any state update. |
|
|
714
|
+
| | | If an error is thrown the React State update won't be fired. |
|
|
715
|
+
| | | ⚠️ Wrap your callback with `useCallback` to avoid unnecessary `IntersectionObserver` recreation. |
|
|
716
|
+
| `options.onEnter` | `OnIntersectHandler` | (Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
|
|
717
|
+
| | | This callback is awaited before any state update. |
|
|
718
|
+
| | | If an error is thrown the React State update won't be fired. |
|
|
719
|
+
| | | ⚠️ Wrap your callback with `useCallback` to avoid unnecessary `IntersectionObserver` recreation. |
|
|
720
|
+
| `options.onExit` | `OnIntersectHandler` | (Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
|
|
721
|
+
| | | This callback is awaited before any state update. |
|
|
722
|
+
| | | If an error is thrown the React State update won't be fired. |
|
|
723
|
+
| | | ⚠️ Wrap your callback with `useCallback` to avoid unnecessary `IntersectionObserver` recreation. |
|
|
724
|
+
|
|
725
|
+
</details>
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
<details>
|
|
730
|
+
|
|
731
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
732
|
+
|
|
733
|
+
Type: `UseInViewReturnType`
|
|
734
|
+
|
|
735
|
+
An object containing:
|
|
736
|
+
|
|
737
|
+
- `inView`: `boolean` - Indicates whether the target Element is in viewport or not.
|
|
738
|
+
- `setInView`: `React.Dispatch<React.SetStateAction<boolean>>` - A React Dispatch SetState action that allows custom state updates.
|
|
739
|
+
- `enabled`: `boolean` - Indicates whether the target Element is being observed or not.
|
|
740
|
+
- `setEnabled`: `React.Dispatch<React.SetStateAction<boolean>>` - A React Dispatch SetState action that allows to enable/disable observation when needed.
|
|
741
|
+
- `observer`: `IntersectionObserver | undefined` - The `IntersectionObserver` instance. It could be `undefined` if `IntersectionObserver` is not available or observation is not enabled.
|
|
742
|
+
|
|
743
|
+
</details>
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
<details>
|
|
748
|
+
|
|
749
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
750
|
+
|
|
751
|
+
###### Basic usage
|
|
752
|
+
|
|
753
|
+
```tsx
|
|
754
|
+
'use client'
|
|
755
|
+
|
|
756
|
+
import { useRef } from 'react'
|
|
757
|
+
import { useInView } from '@alessiofrittoli/react-hooks'
|
|
758
|
+
|
|
759
|
+
const UseInViewExample: React.FC = () => {
|
|
760
|
+
|
|
761
|
+
const targetRef = useRef<HTMLDivElement>( null )
|
|
762
|
+
const { inView } = useInView( ref )
|
|
763
|
+
|
|
764
|
+
return (
|
|
765
|
+
Array.from( Array( 6 ) ).map( ( value, index ) => (
|
|
766
|
+
<div
|
|
767
|
+
key={ index }
|
|
768
|
+
style={ {
|
|
769
|
+
height : '50vh',
|
|
770
|
+
border : '1px solid red',
|
|
771
|
+
display : 'flex',
|
|
772
|
+
alignItems : 'center',
|
|
773
|
+
justifyContent : 'center',
|
|
774
|
+
} }
|
|
775
|
+
>
|
|
776
|
+
<div
|
|
777
|
+
ref={ index === 2 ? targetRef : undefined }
|
|
778
|
+
style={ {
|
|
779
|
+
width : 150,
|
|
780
|
+
height : 150,
|
|
781
|
+
borderRadius : 12,
|
|
782
|
+
display : 'flex',
|
|
783
|
+
alignItems : 'center',
|
|
784
|
+
justifyContent : 'center',
|
|
785
|
+
background : inView ? '#51AF83' : '#201A1B',
|
|
786
|
+
color : inView ? '#201A1B' : '#FFFFFF',
|
|
787
|
+
} }
|
|
788
|
+
>{ index + 1 }</div>
|
|
789
|
+
</div>
|
|
790
|
+
) )
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
###### Disconnect observer after target enters the viewport
|
|
799
|
+
|
|
800
|
+
```tsx
|
|
801
|
+
'use client'
|
|
802
|
+
|
|
803
|
+
import { useRef } from 'react'
|
|
804
|
+
import { useInView } from '@alessiofrittoli/react-hooks'
|
|
805
|
+
|
|
806
|
+
const OnceExample: React.FC = () => {
|
|
807
|
+
|
|
808
|
+
const targetRef = useRef<HTMLDivElement>( null )
|
|
809
|
+
const { inView } = useInView( targetRef, { once: true } )
|
|
810
|
+
|
|
811
|
+
useEffect( () => {
|
|
812
|
+
|
|
813
|
+
if ( ! inView ) return
|
|
814
|
+
console.count( 'Fired only once: element entered viewport.' )
|
|
815
|
+
|
|
816
|
+
}, [ inView ] )
|
|
817
|
+
|
|
818
|
+
return (
|
|
819
|
+
<div
|
|
820
|
+
ref={ targetRef }
|
|
821
|
+
style={ {
|
|
822
|
+
height : 200,
|
|
823
|
+
background : inView ? 'lime' : 'gray',
|
|
824
|
+
} }
|
|
825
|
+
/>
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
###### Observe target only when needed
|
|
834
|
+
|
|
835
|
+
```tsx
|
|
836
|
+
'use client'
|
|
837
|
+
|
|
838
|
+
import { useRef } from 'react'
|
|
839
|
+
import { useInView } from '@alessiofrittoli/react-hooks'
|
|
840
|
+
|
|
841
|
+
const OnDemandObservation: React.FC = () => {
|
|
842
|
+
|
|
843
|
+
const targetRef = useRef<HTMLDivElement>( null )
|
|
844
|
+
const {
|
|
845
|
+
inView, enabled, setEnabled
|
|
846
|
+
} = useInView( targetRef, { enable: false } )
|
|
847
|
+
|
|
848
|
+
return (
|
|
849
|
+
<div>
|
|
850
|
+
<button onClick={ () => setEnabled( prev => ! prev ) }>
|
|
851
|
+
{ enabled ? 'Disconnect observer' : 'Observe' }
|
|
852
|
+
</button>
|
|
853
|
+
<div
|
|
854
|
+
ref={ targetRef }
|
|
855
|
+
style={ {
|
|
856
|
+
height : 200,
|
|
857
|
+
marginTop : 50,
|
|
858
|
+
background : inView ? 'lime' : 'gray',
|
|
859
|
+
} }
|
|
860
|
+
/>
|
|
861
|
+
</div>
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
---
|
|
868
|
+
|
|
869
|
+
###### Execute custom callback when intersection occurs
|
|
870
|
+
|
|
871
|
+
```tsx
|
|
872
|
+
'use client'
|
|
873
|
+
|
|
874
|
+
import { useRef } from 'react'
|
|
875
|
+
import { useInView, type OnIntersectStateHandler } from '@alessiofrittoli/react-hooks'
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
const AsyncStartExample: React.FC = () => {
|
|
879
|
+
|
|
880
|
+
const targetRef = useRef<HTMLDivElement>( null )
|
|
881
|
+
const onIntersect = useCallback<OnIntersectStateHandler>( async ( { entry, isEntering } ) => {
|
|
882
|
+
|
|
883
|
+
if ( isEntering ) {
|
|
884
|
+
console.log( 'Delaying state update...' )
|
|
885
|
+
await new Promise( resolve => setTimeout( resolve, 1000 ) ) // Simulate delay
|
|
886
|
+
console.log( 'Async task completed. `inView` will now be updated.' )
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
console.log( 'Delaying state update...' )
|
|
891
|
+
await new Promise( resolve => setTimeout( resolve, 1000 ) ) // Simulate delay
|
|
892
|
+
console.log( 'Async task completed. `inView` will now be updated.' )
|
|
893
|
+
|
|
894
|
+
}, [] )
|
|
895
|
+
|
|
896
|
+
const { inView } = useInView( targetRef, { onIntersect } )
|
|
897
|
+
|
|
898
|
+
return (
|
|
899
|
+
<div
|
|
900
|
+
ref={ targetRef }
|
|
901
|
+
style={ {
|
|
902
|
+
height : 200,
|
|
903
|
+
background : inView ? 'lime' : 'gray',
|
|
904
|
+
} }
|
|
905
|
+
/>
|
|
906
|
+
)
|
|
907
|
+
}
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
---
|
|
911
|
+
|
|
912
|
+
###### Execute custom callback when `onEnter` and `onExit`
|
|
913
|
+
|
|
914
|
+
```tsx
|
|
915
|
+
'use client'
|
|
916
|
+
|
|
917
|
+
import { useRef } from 'react'
|
|
918
|
+
import { useInView, type OnIntersectHandler } from '@alessiofrittoli/react-hooks'
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
const AsyncStartExample: React.FC = () => {
|
|
922
|
+
|
|
923
|
+
const targetRef = useRef<HTMLDivElement>( null )
|
|
924
|
+
const onEnter = useCallback<OnIntersectHandler>( async ( { entry } ) => {
|
|
925
|
+
console.log( 'In viewport - ', entry )
|
|
926
|
+
}, [] )
|
|
927
|
+
const onExit = useCallback<OnIntersectHandler>( async ( { entry } ) => {
|
|
928
|
+
console.log( 'Exited viewport - ', entry )
|
|
929
|
+
}, [] )
|
|
930
|
+
|
|
931
|
+
const { inView } = useInView( targetRef, { onEnter, onExit } )
|
|
932
|
+
|
|
933
|
+
return (
|
|
934
|
+
<div
|
|
935
|
+
ref={ targetRef }
|
|
936
|
+
style={ {
|
|
937
|
+
height : 200,
|
|
938
|
+
background : inView ? 'lime' : 'gray',
|
|
939
|
+
} }
|
|
940
|
+
/>
|
|
941
|
+
)
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
</details>
|
|
946
|
+
|
|
947
|
+
---
|
|
948
|
+
|
|
949
|
+
#### Miscellaneous
|
|
950
|
+
|
|
951
|
+
---
|
|
952
|
+
|
|
953
|
+
##### `useInput`
|
|
954
|
+
|
|
955
|
+
Docs coming soon
|
|
956
|
+
|
|
957
|
+
---
|
|
958
|
+
|
|
959
|
+
##### `useEffectOnce`
|
|
960
|
+
|
|
961
|
+
Docs coming soon
|
|
962
|
+
|
|
963
|
+
---
|
|
964
|
+
|
|
965
|
+
##### `useIsClient`
|
|
966
|
+
|
|
967
|
+
Check if the React Hook or Component where this hook is executed is running in a browser environment.
|
|
968
|
+
|
|
969
|
+
This is pretty usefull to avoid hydration errors.
|
|
970
|
+
|
|
971
|
+
<details>
|
|
972
|
+
|
|
973
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
974
|
+
|
|
975
|
+
Type: `boolean`
|
|
976
|
+
|
|
977
|
+
- `true` if the React Hook or Component is running in a browser environment.
|
|
978
|
+
- `false` otherwise.
|
|
979
|
+
|
|
980
|
+
</details>
|
|
981
|
+
|
|
982
|
+
---
|
|
983
|
+
|
|
984
|
+
<details>
|
|
985
|
+
|
|
986
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
987
|
+
|
|
988
|
+
###### Basic usage
|
|
989
|
+
|
|
990
|
+
```tsx
|
|
991
|
+
'use client'
|
|
992
|
+
|
|
993
|
+
import { useIsClient } from '@alessiofrittoli/react-hooks'
|
|
994
|
+
|
|
995
|
+
export const ClientComponent: React.FC = () => {
|
|
996
|
+
|
|
997
|
+
const isClient = useIsClient()
|
|
998
|
+
|
|
999
|
+
return (
|
|
1000
|
+
<div>Running { ! isClient ? 'server' : 'client' }-side</div>
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
}
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
</details>
|
|
1007
|
+
|
|
1008
|
+
---
|
|
1009
|
+
|
|
1010
|
+
##### `useIsFirstRender`
|
|
1011
|
+
|
|
1012
|
+
Check if is first React Hook/Component render.
|
|
1013
|
+
|
|
1014
|
+
<details>
|
|
1015
|
+
|
|
1016
|
+
<summary style="cursor:pointer">Returns</summary>
|
|
1017
|
+
|
|
1018
|
+
Type: `boolean`
|
|
1019
|
+
|
|
1020
|
+
- `true` at the mount time.
|
|
1021
|
+
- `false` otherwise.
|
|
1022
|
+
|
|
1023
|
+
Note that if the React Hook/Component has no state updates, `useIsFirstRender` will always return `true`.
|
|
1024
|
+
|
|
1025
|
+
</details>
|
|
1026
|
+
|
|
1027
|
+
---
|
|
1028
|
+
|
|
1029
|
+
<details>
|
|
1030
|
+
|
|
1031
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
1032
|
+
|
|
1033
|
+
###### Basic usage
|
|
1034
|
+
|
|
1035
|
+
```tsx
|
|
1036
|
+
'use client'
|
|
1037
|
+
|
|
1038
|
+
import { useIsFirstRender } from '@alessiofrittoli/react-hooks'
|
|
1039
|
+
|
|
1040
|
+
export const ClientComponent: React.FC = () => {
|
|
1041
|
+
|
|
1042
|
+
const isFirstRender = useIsFirstRender()
|
|
1043
|
+
const [ counter, setCounter ] = useState( 0 )
|
|
1044
|
+
|
|
1045
|
+
useEffect( () => {
|
|
1046
|
+
const intv = setInterval( () => {
|
|
1047
|
+
setCounter( prev => prev + 1 )
|
|
1048
|
+
}, 1000 )
|
|
1049
|
+
return () => clearInterval( intv )
|
|
1050
|
+
}, [] )
|
|
1051
|
+
|
|
1052
|
+
return (
|
|
1053
|
+
<div>
|
|
1054
|
+
{ isFirstRender ? 'First render' : 'Subsequent render' }
|
|
1055
|
+
<hr />
|
|
1056
|
+
{ counter }
|
|
1057
|
+
</div>
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
}
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
</details>
|
|
1064
|
+
|
|
1065
|
+
---
|
|
1066
|
+
|
|
1067
|
+
##### `useUpdateEffect`
|
|
1068
|
+
|
|
1069
|
+
Modified version of `useEffect` that skips the first render.
|
|
1070
|
+
|
|
1071
|
+
<details>
|
|
1072
|
+
|
|
1073
|
+
<summary style="cursor:pointer">Parameters</summary>
|
|
1074
|
+
|
|
1075
|
+
| Parameter | Type | Description |
|
|
1076
|
+
|-----------|------------------------|-------------|
|
|
1077
|
+
| `effect` | `React.EffectCallback` | Imperative function that can return a cleanup function. |
|
|
1078
|
+
| `deps` | `React.DependencyList` | If present, effect will only activate if the values in the list change. |
|
|
1079
|
+
|
|
1080
|
+
</details>
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
<details>
|
|
1085
|
+
|
|
1086
|
+
<summary style="cursor:pointer">Usage</summary>
|
|
1087
|
+
|
|
1088
|
+
###### Basic usage
|
|
1089
|
+
|
|
1090
|
+
```tsx
|
|
1091
|
+
'use client'
|
|
1092
|
+
|
|
1093
|
+
import { useEffect, useState } from 'react'
|
|
1094
|
+
import { useUpdateEffect } from '@alessiofrittoli/react-hooks'
|
|
1095
|
+
|
|
1096
|
+
export const ClientComponent: React.FC = () => {
|
|
1097
|
+
|
|
1098
|
+
const [ count, setCount ] = useState( 0 )
|
|
1099
|
+
|
|
1100
|
+
useEffect( () => {
|
|
1101
|
+
const intv = setInterval( () => {
|
|
1102
|
+
setCount( prev => prev + 1 )
|
|
1103
|
+
}, 1000 )
|
|
1104
|
+
return () => clearInterval( intv )
|
|
1105
|
+
}, [] )
|
|
1106
|
+
|
|
1107
|
+
useEffect( () => {
|
|
1108
|
+
console.log( 'useEffect', count ) // starts from 0
|
|
1109
|
+
return () => {
|
|
1110
|
+
console.log( 'useEffect - clean up', count ) // starts from 0
|
|
1111
|
+
}
|
|
1112
|
+
}, [ count ] )
|
|
1113
|
+
|
|
1114
|
+
useUpdateEffect( () => {
|
|
1115
|
+
console.log( 'useUpdateEffect', count ) // starts from 1
|
|
1116
|
+
return () => {
|
|
1117
|
+
console.log( 'useUpdateEffect - clean up', count ) // starts from 1
|
|
1118
|
+
}
|
|
1119
|
+
}, [ count ] )
|
|
1120
|
+
|
|
1121
|
+
return (
|
|
1122
|
+
<div>{ count }</div>
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
}
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
</details>
|
|
1129
|
+
|
|
1130
|
+
---
|
|
1131
|
+
|
|
1132
|
+
##### `usePagination`
|
|
1133
|
+
|
|
1134
|
+
Get pagination informations based on the given options.
|
|
1135
|
+
|
|
1136
|
+
This hook memoize the returned result of the [`paginate`](https://github.com/alessiofrittoli/math-utils/blob/master/docs/helpers/README.md#paginate) function imported from [`@alessiofrittoli/math-utils`](https://npmjs.com/package/@alessiofrittoli/math-utils).
|
|
1137
|
+
|
|
1138
|
+
See [`paginate`](https://github.com/alessiofrittoli/math-utils/blob/master/docs/helpers/README.md#paginate) function Documentation for more information about it.
|
|
1139
|
+
|
|
1140
|
+
---
|
|
1141
|
+
|
|
1142
|
+
##### `useSelection`
|
|
1143
|
+
|
|
1144
|
+
Docs coming soon
|
|
1145
|
+
|
|
1146
|
+
---
|
|
1147
|
+
|
|
1148
|
+
### Development
|
|
1149
|
+
|
|
1150
|
+
#### Install depenendencies
|
|
1151
|
+
|
|
1152
|
+
```bash
|
|
1153
|
+
npm install
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
or using `pnpm`
|
|
1157
|
+
|
|
1158
|
+
```bash
|
|
1159
|
+
pnpm i
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
#### Build the source code
|
|
1163
|
+
|
|
1164
|
+
Run the following command to test and build code for distribution.
|
|
1165
|
+
|
|
1166
|
+
```bash
|
|
1167
|
+
pnpm build
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
#### [ESLint](https://www.npmjs.com/package/eslint)
|
|
1171
|
+
|
|
1172
|
+
warnings / errors check.
|
|
1173
|
+
|
|
1174
|
+
```bash
|
|
1175
|
+
pnpm lint
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
#### [Jest](https://npmjs.com/package/jest)
|
|
1179
|
+
|
|
1180
|
+
Run all the defined test suites by running the following:
|
|
1181
|
+
|
|
1182
|
+
```bash
|
|
1183
|
+
# Run tests and watch file changes.
|
|
1184
|
+
pnpm test:watch
|
|
1185
|
+
|
|
1186
|
+
# Run tests in a CI environment.
|
|
1187
|
+
pnpm test:ci
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
- See [`package.json`](./package.json) file scripts for more info.
|
|
1191
|
+
|
|
1192
|
+
Run tests with coverage.
|
|
1193
|
+
|
|
1194
|
+
An HTTP server is then started to serve coverage files from `./coverage` folder.
|
|
1195
|
+
|
|
1196
|
+
⚠️ You may see a blank page the first time you run this command. Simply refresh the browser to see the updates.
|
|
1197
|
+
|
|
1198
|
+
```bash
|
|
1199
|
+
test:coverage:serve
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
---
|
|
1203
|
+
|
|
1204
|
+
### Contributing
|
|
1205
|
+
|
|
1206
|
+
Contributions are truly welcome!
|
|
1207
|
+
|
|
1208
|
+
Please refer to the [Contributing Doc](./CONTRIBUTING.md) for more information on how to start contributing to this project.
|
|
1209
|
+
|
|
1210
|
+
Help keep this project up to date with [GitHub Sponsor][sponsor-url].
|
|
1211
|
+
|
|
1212
|
+
[![GitHub Sponsor][sponsor-badge]][sponsor-url]
|
|
1213
|
+
|
|
1214
|
+
---
|
|
1215
|
+
|
|
1216
|
+
### Security
|
|
1217
|
+
|
|
1218
|
+
If you believe you have found a security vulnerability, we encourage you to **_responsibly disclose this and NOT open a public issue_**. We will investigate all legitimate reports. Email `security@alessiofrittoli.it` to disclose any security vulnerabilities.
|
|
1219
|
+
|
|
1220
|
+
### Made with ☕
|
|
1221
|
+
|
|
1222
|
+
<table style='display:flex;gap:20px;'>
|
|
1223
|
+
<tbody>
|
|
1224
|
+
<tr>
|
|
1225
|
+
<td>
|
|
1226
|
+
<img alt="avatar" src='https://avatars.githubusercontent.com/u/35973186' style='width:60px;border-radius:50%;object-fit:contain;'>
|
|
1227
|
+
</td>
|
|
1228
|
+
<td>
|
|
1229
|
+
<table style='display:flex;gap:2px;flex-direction:column;'>
|
|
1230
|
+
<tbody>
|
|
1231
|
+
<tr>
|
|
1232
|
+
<td>
|
|
1233
|
+
<a href='https://github.com/alessiofrittoli' target='_blank' rel='noopener'>Alessio Frittoli</a>
|
|
1234
|
+
</td>
|
|
1235
|
+
</tr>
|
|
1236
|
+
<tr>
|
|
1237
|
+
<td>
|
|
1238
|
+
<small>
|
|
1239
|
+
<a href='https://alessiofrittoli.it' target='_blank' rel='noopener'>https://alessiofrittoli.it</a> |
|
|
1240
|
+
<a href='mailto:info@alessiofrittoli.it' target='_blank' rel='noopener'>info@alessiofrittoli.it</a>
|
|
1241
|
+
</small>
|
|
1242
|
+
</td>
|
|
1243
|
+
</tr>
|
|
1244
|
+
</tbody>
|
|
1245
|
+
</table>
|
|
1246
|
+
</td>
|
|
1247
|
+
</tr>
|
|
1248
|
+
</tbody>
|
|
1249
|
+
</table>
|