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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1223 +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
- ##### `useDarkMode`
327
-
328
- Easily manage dark mode with full respect for user device preferences.
329
-
330
- This hook is user-oriented and built to honor system-level color scheme settings:
331
-
332
- - If the device prefers a dark color scheme, dark mode is automatically enabled on first load.
333
- - If the user enables/disables dark mode via a web widget, the preference is stored in `localStorage` under the key `dark-mode`.
334
- - If the device color scheme preference changes (e.g. via OS settings), that change takes precedence and is stored for future visits.
335
-
336
- <details>
337
-
338
- <summary style="cursor:pointer">Parameters</summary>
339
-
340
- | Parameter | Type | Description |
341
- |-----------|------|-------------|
342
- | `options` | `UseDarkModeOptions` | (Optional) Configuration object for the hook. |
343
- | `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. |
344
- | `options.docClassNames` | `[dark: string, light: string]` | (Optional) Array of class names to toggle on the `<html>` element, e.g. `['dark', 'light']`. |
345
-
346
- </details>
347
-
348
- ---
349
-
350
- <details>
351
-
352
- <summary style="cursor:pointer">Returns</summary>
353
-
354
- Type: `UseDarkModeOutput`
355
-
356
- An object containing utilities for managing dark mode:
357
-
358
- - `isDarkMode`: `boolean` — Whether dark mode is currently enabled.
359
- - `isDarkOS`: `boolean` — Whether the user's system prefers dark mode.
360
- - `toggleDarkMode`: `() => void` — Toggles dark mode and saves the preference.
361
- - `enableDarkMode`: `() => void` — Enables dark mode and saves the preference.
362
- - `disableDarkMode`: `() => void` Disables dark mode and saves the preference.
363
-
364
- </details>
365
-
366
- ---
367
-
368
- <details>
369
-
370
- <summary style="cursor:pointer">Usage</summary>
371
-
372
- ###### Basic usage
373
-
374
- ```tsx
375
- 'use client'
376
-
377
- import { useDarkMode } from '@alessiofrittoli/react-hooks'
378
-
379
- export const Component: React.FC = () => {
380
- const { isDarkMode } = useDarkMode()
381
-
382
- return (
383
- <div>{ isDarkMode ? 'Dark mode enabled' : 'Dark mode disabled' }</div>
384
- )
385
- }
386
- ```
387
-
388
- ---
389
-
390
- ###### Update Document class names for CSS styling
391
-
392
- ```tsx
393
- // Component.tsx
394
- 'use client'
395
-
396
- import { useDarkMode } from '@alessiofrittoli/react-hooks'
397
-
398
- export const Component: React.FC = () => {
399
- const { isDarkMode } = useDarkMode( {
400
- docClassNames: [ 'dark', 'light' ],
401
- } )
402
-
403
- return (
404
- <div>{ isDarkMode ? 'Dark mode enabled' : 'Dark mode disabled' }</div>
405
- )
406
- }
407
- ```
408
-
409
- ```css
410
- /* style.css */
411
- .light {
412
- color-scheme: light;
413
- }
414
-
415
- .dark {
416
- color-scheme: dark;
417
- }
418
-
419
- .light body
420
- {
421
- color : black;
422
- background: white;
423
- }
424
-
425
- .dark body
426
- {
427
- color : white;
428
- background: black;
429
- }
430
- ```
431
-
432
- ---
433
-
434
- ###### Custom theme switcher
435
-
436
- ```tsx
437
- 'use client'
438
-
439
- import { useDarkMode } from '@alessiofrittoli/react-hooks'
440
-
441
- export const ThemeSwitcher: React.FC = () => {
442
- const { isDarkMode, toggleDarkMode } = useDarkMode()
443
-
444
- return (
445
- <button onClick={ toggleDarkMode }>
446
- { isDarkMode ? '🌙' : '☀️' }
447
- </button>
448
- )
449
- }
450
- ```
451
-
452
- ---
453
-
454
- ###### Sync Document theme-color for consistent browser styling
455
-
456
- Browsers automatically apply colorization using:
457
-
458
- ```html
459
- <meta name='theme-color' media='(prefers-color-scheme: dark)' />
460
- ```
461
-
462
- 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.
463
-
464
- To ensure consistency, `useDarkMode` updates these meta tags dynamically based on the actual mode.
465
-
466
- Just make sure to define both `light` and `dark` theme-color tags in your document:
467
-
468
- ```html
469
- <head>
470
- <meta name='theme-color' media='(prefers-color-scheme: light)' content='lime'>
471
- <meta name='theme-color' media='(prefers-color-scheme: dark)' content='aqua'>
472
- </head>
473
- ```
474
-
475
- </details>
476
-
477
- ---
478
-
479
- ##### `useIsPortrait`
480
-
481
- Check if device is portrait oriented.
482
-
483
- React State get updated when device orientation changes.
484
-
485
- <details>
486
-
487
- <summary style="cursor:pointer">Returns</summary>
488
-
489
- Type: `boolean`
490
-
491
- - `true` if the device is portrait oriented.
492
- - `false` otherwise.
493
-
494
- </details>
495
-
496
- ---
497
-
498
- <details>
499
-
500
- <summary style="cursor:pointer">Usage</summary>
501
-
502
- ###### Check if user device is in landscape
503
-
504
- ```tsx
505
- import { useIsPortrait } from '@alessiofrittoli/react-hooks'
506
-
507
- const isLandscape = ! useIsPortrait()
508
- ```
509
-
510
- </details>
511
-
512
- ---
513
-
514
- #### DOM API
515
-
516
- ##### `useScrollBlock`
517
-
518
- Prevent Element overflow.
519
-
520
- <details>
521
-
522
- <summary style="cursor:pointer">Parameters</summary>
523
-
524
- | Parameter | Type | Default | Description |
525
- |-----------|------|---------|-------------|
526
- | `target` | `React.RefObject<HTMLElement\|null>` | `Document.documentElement` | (Optional) The React RefObject target HTMLElement. |
527
-
528
- </details>
529
-
530
- ---
531
-
532
- <details>
533
-
534
- <summary style="cursor:pointer">Returns</summary>
535
-
536
- Type: `[ () => void, () => void ]`
537
-
538
- A tuple with block and restore scroll callbacks.
539
-
540
- </details>
541
-
542
- ---
543
-
544
- <details>
545
-
546
- <summary style="cursor:pointer">Usage</summary>
547
-
548
- ###### Block Document Overflow
549
-
550
- ```tsx
551
- import { useScrollBlock } from '@alessiofrittoli/react-hooks'
552
-
553
- const [ blockScroll, restoreScroll ] = useScrollBlock()
554
-
555
- const openPopUpHandler = useCallback( () => {
556
- ...
557
- blockScroll()
558
- }, [ blockScroll ] )
559
-
560
- const closePopUpHandler = useCallback( () => {
561
- ...
562
- restoreScroll()
563
- }, [ restoreScroll ] )
564
-
565
- ...
566
- ```
567
-
568
- ---
569
-
570
- ###### Block HTML Element Overflow
571
-
572
- ```tsx
573
- const elementRef = useRef<HTMLDivElement>( null )
574
-
575
- const [ blockScroll, restoreScroll ] = useScrollBlock( elementRef )
576
-
577
- const scrollBlockHandler = useCallback( () => {
578
- ...
579
- blockScroll()
580
- }, [ blockScroll ] )
581
-
582
- const scrollRestoreHandler = useCallback( () => {
583
- ...
584
- restoreScroll()
585
- }, [ restoreScroll ] )
586
-
587
- ...
588
- ```
589
-
590
- </details>
591
-
592
- ---
593
-
594
- ##### `useFocusTrap`
595
-
596
- Trap focus inside the given HTML Element.
597
-
598
- This comes pretty handy when rendering a modal that shouldn't be closed without a user required action.
599
-
600
- <details>
601
-
602
- <summary style="cursor:pointer">Parameters</summary>
603
-
604
- | Parameter | Type | Description |
605
- |-----------|------|-------------|
606
- | `target` | `React.RefObject<HTMLElement\|null>` | The target HTMLElement React RefObject to trap focus within. |
607
- | | | If no target is given, you must provide the target HTMLElement when calling `setFocusTrap`. |
608
-
609
- </details>
610
-
611
- ---
612
-
613
- <details>
614
-
615
- <summary style="cursor:pointer">Returns</summary>
616
-
617
- Type: `readonly [ SetFocusTrap, RestoreFocusTrap ]`
618
-
619
- A tuple containing:
620
-
621
- - `setFocusTrap`: A function to enable the focus trap. Optionally accept an HTMLElement as target.
622
- - `restoreFocusTrap`: A function to restore the previous focus state.
623
-
624
- </details>
625
-
626
- ---
627
-
628
- <details>
629
-
630
- <summary style="cursor:pointer">Usage</summary>
631
-
632
- ###### Defining the target on hook initialization
633
-
634
- ```tsx
635
- import { useFocusTrap } from '@alessiofrittoli/react-hooks'
636
-
637
- const modalRef = useRef<HTMLDivElement>( null )
638
- const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap( modalRef )
639
-
640
- const modalOpenHandler = useCallback( () => {
641
- if ( ! modalRef.current ) return
642
- // ... open modal
643
- setFocusTrap()
644
- modalRef.current.focus() // focus the dialog so next tab will focus the next element inside the modal
645
- }, [ setFocusTrap ] )
646
-
647
- const modalCloseHandler = useCallback( () => {
648
- // ... close modal
649
- restoreFocusTrap() // cancel focus trap and restore focus to the last active element before enablig the focus trap
650
- }, [ restoreFocusTrap ] )
651
- ```
652
-
653
- ---
654
-
655
- ###### Defining the target ondemand
656
-
657
- ```tsx
658
- import { useFocusTrap } from '@alessiofrittoli/react-hooks'
659
-
660
- const modalRef = useRef<HTMLDivElement>( null )
661
- const modal2Ref = useRef<HTMLDivElement>( null )
662
- const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap()
663
-
664
- const modalOpenHandler = useCallback( () => {
665
- if ( ! modalRef.current ) return
666
- // ... open modal
667
- setFocusTrap( modalRef.current )
668
- modalRef.current.focus()
669
- }, [ setFocusTrap ] )
670
-
671
- const modal2OpenHandler = useCallback( () => {
672
- if ( ! modal2Ref.current ) return
673
- // ... open modal
674
- setFocusTrap( modal2Ref.current )
675
- modal2Ref.current.focus()
676
- }, [ setFocusTrap ] )
677
- ```
678
-
679
- </details>
680
-
681
- ---
682
-
683
- ##### `useInView`
684
-
685
- Check if the given target Element is intersecting with an ancestor Element or with a top-level document's viewport.
686
-
687
- <details>
688
-
689
- <summary style="cursor:pointer">Parameters</summary>
690
-
691
- | Parameter | Type | Description |
692
- |-----------|------|-------------|
693
- | `target` | `React.RefObject<Element\|null>` | The React.RefObject of the target Element to observe. |
694
- | `options` | `UseInViewOptions` | (Optional) An object defining custom `IntersectionObserver` options. |
695
- | `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. |
696
- | `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. |
697
- | `options.amount` | `'all'\|'some'\|number\|number[]` | (Optional) The intersecting target thresholds. |
698
- | | | Threshold can be set to: |
699
- | | | - `all` - `1` will be used. |
700
- | | | - `some` - `0.5` will be used. |
701
- | | | - `number` |
702
- | | | - `number[]` |
703
- | `options.once` | `boolean` | (Optional) By setting this to `true` the observer will be disconnected after the target Element enters the viewport. |
704
- | `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`. |
705
- | `options.enable` | `boolean` | (Optional) Defines the initial observation activity. Use the returned `setEnabled` to update this state. Default: `true`. |
706
- | `options.onIntersect` | `OnIntersectStateHandler` | (Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
707
- | | | This callback is awaited before any state update. |
708
- | | | If an error is thrown the React State update won't be fired. |
709
- | | | ⚠️ Wrap your callback with `useCallback` to avoid unnecessary `IntersectionObserver` recreation. |
710
- | `options.onEnter` | `OnIntersectHandler` | (Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
711
- | | | This callback is awaited before any state update. |
712
- | | | If an error is thrown the React State update won't be fired. |
713
- | | | ⚠️ Wrap your callback with `useCallback` to avoid unnecessary `IntersectionObserver` recreation. |
714
- | `options.onExit` | `OnIntersectHandler` | (Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
715
- | | | This callback is awaited before any state update. |
716
- | | | If an error is thrown the React State update won't be fired. |
717
- | | | ⚠️ Wrap your callback with `useCallback` to avoid unnecessary `IntersectionObserver` recreation. |
718
-
719
- </details>
720
-
721
- ---
722
-
723
- <details>
724
-
725
- <summary style="cursor:pointer">Returns</summary>
726
-
727
- Type: `UseInViewReturnType`
728
-
729
- An object containing:
730
-
731
- - `inView`: `boolean` - Indicates whether the target Element is in viewport or not.
732
- - `setInView`: `React.Dispatch<React.SetStateAction<boolean>>` - A React Dispatch SetState action that allows custom state updates.
733
- - `enabled`: `boolean` - Indicates whether the target Element is being observed or not.
734
- - `setEnabled`: `React.Dispatch<React.SetStateAction<boolean>>` - A React Dispatch SetState action that allows to enable/disable observation when needed.
735
- - `observer`: `IntersectionObserver | undefined` - The `IntersectionObserver` instance. It could be `undefined` if `IntersectionObserver` is not available or observation is not enabled.
736
-
737
- </details>
738
-
739
- ---
740
-
741
- <details>
742
-
743
- <summary style="cursor:pointer">Usage</summary>
744
-
745
- ###### Basic usage
746
-
747
- ```tsx
748
- 'use client'
749
-
750
- import { useRef } from 'react'
751
- import { useInView } from '@alessiofrittoli/react-hooks'
752
-
753
- const UseInViewExample: React.FC = () => {
754
-
755
- const targetRef = useRef<HTMLDivElement>( null )
756
- const { inView } = useInView( ref )
757
-
758
- return (
759
- Array.from( Array( 6 ) ).map( ( value, index ) => (
760
- <div
761
- key={ index }
762
- style={ {
763
- height : '50vh',
764
- border : '1px solid red',
765
- display : 'flex',
766
- alignItems : 'center',
767
- justifyContent : 'center',
768
- } }
769
- >
770
- <div
771
- ref={ index === 2 ? targetRef : undefined }
772
- style={ {
773
- width : 150,
774
- height : 150,
775
- borderRadius : 12,
776
- display : 'flex',
777
- alignItems : 'center',
778
- justifyContent : 'center',
779
- background : inView ? '#51AF83' : '#201A1B',
780
- color : inView ? '#201A1B' : '#FFFFFF',
781
- } }
782
- >{ index + 1 }</div>
783
- </div>
784
- ) )
785
- )
786
-
787
- }
788
- ```
789
-
790
- ---
791
-
792
- ###### Disconnect observer after target enters the viewport
793
-
794
- ```tsx
795
- 'use client'
796
-
797
- import { useRef } from 'react'
798
- import { useInView } from '@alessiofrittoli/react-hooks'
799
-
800
- const OnceExample: React.FC = () => {
801
-
802
- const targetRef = useRef<HTMLDivElement>( null )
803
- const { inView } = useInView( targetRef, { once: true } )
804
-
805
- useEffect( () => {
806
-
807
- if ( ! inView ) return
808
- console.count( 'Fired only once: element entered viewport.' )
809
-
810
- }, [ inView ] )
811
-
812
- return (
813
- <div
814
- ref={ targetRef }
815
- style={ {
816
- height : 200,
817
- background : inView ? 'lime' : 'gray',
818
- } }
819
- />
820
- )
821
-
822
- }
823
- ```
824
-
825
- ---
826
-
827
- ###### Observe target only when needed
828
-
829
- ```tsx
830
- 'use client'
831
-
832
- import { useRef } from 'react'
833
- import { useInView } from '@alessiofrittoli/react-hooks'
834
-
835
- const OnDemandObservation: React.FC = () => {
836
-
837
- const targetRef = useRef<HTMLDivElement>( null )
838
- const {
839
- inView, enabled, setEnabled
840
- } = useInView( targetRef, { enable: false } )
841
-
842
- return (
843
- <div>
844
- <button onClick={ () => setEnabled( prev => ! prev ) }>
845
- { enabled ? 'Disconnect observer' : 'Observe' }
846
- </button>
847
- <div
848
- ref={ targetRef }
849
- style={ {
850
- height : 200,
851
- marginTop : 50,
852
- background : inView ? 'lime' : 'gray',
853
- } }
854
- />
855
- </div>
856
- )
857
-
858
- }
859
- ```
860
-
861
- ---
862
-
863
- ###### Execute custom callback when intersection occurs
864
-
865
- ```tsx
866
- 'use client'
867
-
868
- import { useRef } from 'react'
869
- import { useInView, type OnIntersectStateHandler } from '@alessiofrittoli/react-hooks'
870
-
871
-
872
- const AsyncStartExample: React.FC = () => {
873
-
874
- const targetRef = useRef<HTMLDivElement>( null )
875
- const onIntersect = useCallback<OnIntersectStateHandler>( async ( { entry, isEntering } ) => {
876
-
877
- if ( isEntering ) {
878
- console.log( 'Delaying state update...' )
879
- await new Promise( resolve => setTimeout( resolve, 1000 ) ) // Simulate delay
880
- console.log( 'Async task completed. `inView` will now be updated.' )
881
- return
882
- }
883
-
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
-
888
- }, [] )
889
-
890
- const { inView } = useInView( targetRef, { onIntersect } )
891
-
892
- return (
893
- <div
894
- ref={ targetRef }
895
- style={ {
896
- height : 200,
897
- background : inView ? 'lime' : 'gray',
898
- } }
899
- />
900
- )
901
- }
902
- ```
903
-
904
- ---
905
-
906
- ###### Execute custom callback when `onEnter` and `onExit`
907
-
908
- ```tsx
909
- 'use client'
910
-
911
- import { useRef } from 'react'
912
- import { useInView, type OnIntersectHandler } from '@alessiofrittoli/react-hooks'
913
-
914
-
915
- const AsyncStartExample: React.FC = () => {
916
-
917
- const targetRef = useRef<HTMLDivElement>( null )
918
- const onEnter = useCallback<OnIntersectHandler>( async ( { entry } ) => {
919
- console.log( 'In viewport - ', entry )
920
- }, [] )
921
- const onExit = useCallback<OnIntersectHandler>( async ( { entry } ) => {
922
- console.log( 'Exited viewport - ', entry )
923
- }, [] )
924
-
925
- const { inView } = useInView( targetRef, { onEnter, onExit } )
926
-
927
- return (
928
- <div
929
- ref={ targetRef }
930
- style={ {
931
- height : 200,
932
- background : inView ? 'lime' : 'gray',
933
- } }
934
- />
935
- )
936
- }
937
- ```
938
-
939
- </details>
940
-
941
- ---
942
-
943
- #### Miscellaneous
944
-
945
- ##### `useIsClient`
946
-
947
- Check if the React Hook or Component where this hook is executed is running in a browser environment.
948
-
949
- This is pretty usefull to avoid hydration errors.
950
-
951
- <details>
952
-
953
- <summary style="cursor:pointer">Returns</summary>
954
-
955
- Type: `boolean`
956
-
957
- - `true` if the React Hook or Component is running in a browser environment.
958
- - `false` otherwise.
959
-
960
- </details>
961
-
962
- ---
963
-
964
- <details>
965
-
966
- <summary style="cursor:pointer">Usage</summary>
967
-
968
- ###### Basic usage
969
-
970
- ```tsx
971
- 'use client'
972
-
973
- import { useIsClient } from '@alessiofrittoli/react-hooks'
974
-
975
- export const ClientComponent: React.FC = () => {
976
-
977
- const isClient = useIsClient()
978
-
979
- return (
980
- <div>Running { ! isClient ? 'server' : 'client' }-side</div>
981
- )
982
-
983
- }
984
- ```
985
-
986
- </details>
987
-
988
- ---
989
-
990
- ##### `useIsFirstRender`
991
-
992
- Check if is first React Hook/Component render.
993
-
994
- <details>
995
-
996
- <summary style="cursor:pointer">Returns</summary>
997
-
998
- Type: `boolean`
999
-
1000
- - `true` at the mount time.
1001
- - `false` otherwise.
1002
-
1003
- Note that if the React Hook/Component has no state updates, `useIsFirstRender` will always return `true`.
1004
-
1005
- </details>
1006
-
1007
- ---
1008
-
1009
- <details>
1010
-
1011
- <summary style="cursor:pointer">Usage</summary>
1012
-
1013
- ###### Basic usage
1014
-
1015
- ```tsx
1016
- 'use client'
1017
-
1018
- import { useIsFirstRender } from '@alessiofrittoli/react-hooks'
1019
-
1020
- export const ClientComponent: React.FC = () => {
1021
-
1022
- const isFirstRender = useIsFirstRender()
1023
- const [ counter, setCounter ] = useState( 0 )
1024
-
1025
- useEffect( () => {
1026
- const intv = setInterval( () => {
1027
- setCounter( prev => prev + 1 )
1028
- }, 1000 )
1029
- return () => clearInterval( intv )
1030
- }, [] )
1031
-
1032
- return (
1033
- <div>
1034
- { isFirstRender ? 'First render' : 'Subsequent render' }
1035
- <hr />
1036
- { counter }
1037
- </div>
1038
- )
1039
-
1040
- }
1041
- ```
1042
-
1043
- </details>
1044
-
1045
- ---
1046
-
1047
- ##### `useUpdateEffect`
1048
-
1049
- Modified version of `useEffect` that skips the first render.
1050
-
1051
- <details>
1052
-
1053
- <summary style="cursor:pointer">Parameters</summary>
1054
-
1055
- | Parameter | Type | Description |
1056
- |-----------|------------------------|-------------|
1057
- | `effect` | `React.EffectCallback` | Imperative function that can return a cleanup function. |
1058
- | `deps` | `React.DependencyList` | If present, effect will only activate if the values in the list change. |
1059
-
1060
- </details>
1061
-
1062
- ---
1063
-
1064
- <details>
1065
-
1066
- <summary style="cursor:pointer">Usage</summary>
1067
-
1068
- ###### Basic usage
1069
-
1070
- ```tsx
1071
- 'use client'
1072
-
1073
- import { useEffect, useState } from 'react'
1074
- import { useUpdateEffect } from '@alessiofrittoli/react-hooks'
1075
-
1076
- export const ClientComponent: React.FC = () => {
1077
-
1078
- const [ count, setCount ] = useState( 0 )
1079
-
1080
- useEffect( () => {
1081
- const intv = setInterval( () => {
1082
- setCount( prev => prev + 1 )
1083
- }, 1000 )
1084
- return () => clearInterval( intv )
1085
- }, [] )
1086
-
1087
- useEffect( () => {
1088
- console.log( 'useEffect', count ) // starts from 0
1089
- return () => {
1090
- console.log( 'useEffect - clean up', count ) // starts from 0
1091
- }
1092
- }, [ count ] )
1093
-
1094
- useUpdateEffect( () => {
1095
- console.log( 'useUpdateEffect', count ) // starts from 1
1096
- return () => {
1097
- console.log( 'useUpdateEffect - clean up', count ) // starts from 1
1098
- }
1099
- }, [ count ] )
1100
-
1101
- return (
1102
- <div>{ count }</div>
1103
- )
1104
-
1105
- }
1106
- ```
1107
-
1108
- </details>
1109
-
1110
- ---
1111
-
1112
- ##### `usePagination`
1113
-
1114
- Get pagination informations based on the given options.
1115
-
1116
- 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).
1117
-
1118
- See [`paginate`](https://github.com/alessiofrittoli/math-utils/blob/master/docs/helpers/README.md#paginate) function Documentation for more information about it.
1119
-
1120
- ---
1121
-
1122
- ### Development
1123
-
1124
- #### Install depenendencies
1125
-
1126
- ```bash
1127
- npm install
1128
- ```
1129
-
1130
- or using `pnpm`
1131
-
1132
- ```bash
1133
- pnpm i
1134
- ```
1135
-
1136
- #### Build the source code
1137
-
1138
- Run the following command to test and build code for distribution.
1139
-
1140
- ```bash
1141
- pnpm build
1142
- ```
1143
-
1144
- #### [ESLint](https://www.npmjs.com/package/eslint)
1145
-
1146
- warnings / errors check.
1147
-
1148
- ```bash
1149
- pnpm lint
1150
- ```
1151
-
1152
- #### [Jest](https://npmjs.com/package/jest)
1153
-
1154
- Run all the defined test suites by running the following:
1155
-
1156
- ```bash
1157
- # Run tests and watch file changes.
1158
- pnpm test:watch
1159
-
1160
- # Run tests in a CI environment.
1161
- pnpm test:ci
1162
- ```
1163
-
1164
- - See [`package.json`](./package.json) file scripts for more info.
1165
-
1166
- Run tests with coverage.
1167
-
1168
- An HTTP server is then started to serve coverage files from `./coverage` folder.
1169
-
1170
- ⚠️ You may see a blank page the first time you run this command. Simply refresh the browser to see the updates.
1171
-
1172
- ```bash
1173
- test:coverage:serve
1174
- ```
1175
-
1176
- ---
1177
-
1178
- ### Contributing
1179
-
1180
- Contributions are truly welcome!
1181
-
1182
- Please refer to the [Contributing Doc](./CONTRIBUTING.md) for more information on how to start contributing to this project.
1183
-
1184
- Help keep this project up to date with [GitHub Sponsor][sponsor-url].
1185
-
1186
- [![GitHub Sponsor][sponsor-badge]][sponsor-url]
1187
-
1188
- ---
1189
-
1190
- ### Security
1191
-
1192
- 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.
1193
-
1194
- ### Made with
1195
-
1196
- <table style='display:flex;gap:20px;'>
1197
- <tbody>
1198
- <tr>
1199
- <td>
1200
- <img alt="avatar" src='https://avatars.githubusercontent.com/u/35973186' style='width:60px;border-radius:50%;object-fit:contain;'>
1201
- </td>
1202
- <td>
1203
- <table style='display:flex;gap:2px;flex-direction:column;'>
1204
- <tbody>
1205
- <tr>
1206
- <td>
1207
- <a href='https://github.com/alessiofrittoli' target='_blank' rel='noopener'>Alessio Frittoli</a>
1208
- </td>
1209
- </tr>
1210
- <tr>
1211
- <td>
1212
- <small>
1213
- <a href='https://alessiofrittoli.it' target='_blank' rel='noopener'>https://alessiofrittoli.it</a> |
1214
- <a href='mailto:info@alessiofrittoli.it' target='_blank' rel='noopener'>info@alessiofrittoli.it</a>
1215
- </small>
1216
- </td>
1217
- </tr>
1218
- </tbody>
1219
- </table>
1220
- </td>
1221
- </tr>
1222
- </tbody>
1223
- </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>