@alessiofrittoli/react-hooks 3.2.0 → 3.3.0-alpha.2

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