Run the following command to start using react-hooks
in your projects:
npm i @alessiofrittoli/react-hooks
or using pnpm
pnpm i @alessiofrittoli/react-hooks
This library may define and exports hooks that requires additional ESLint configuration for your project such as useUpdateEffect
.
Simply imports recommended configuration from @alessiofrittoli/react-hooks/eslint
and add them to your ESLint configuration like so:
import { config as AFReactHooksEslint } from '@alessiofrittoli/react-hooks/eslint'
/** @type {import('eslint').Linter.Config[]} */
const config = [
...AFReactHooksEslint.recommended,
// ... other configurations
]
export default config
The following storage hooks use Storage Utilities from @alessiofrittoli/web-utils
adding a React oriented implementation.
Easly handle Local or Session Storage State.
Type parameters
Parameter | Type | Default | Description |
---|---|---|---|
T |
any |
string |
A custom type applied to the stored item. |
Parameters
Parameter | Type | Default | Description |
---|---|---|---|
key |
string |
- | The storage item key. |
initial |
T |
- | The storage item initial value. |
type |
local|session |
local | (Optional) The storage API to use. |
Returns
Type: [ Value<T>, SetValue<Value<T>> ]
A tuple with the stored item value or initial value and the setter function.
Usage
import {
useStorage, useLocalStorage, useSessionStorage
} from '@alessiofrittoli/react-hooks'
'use client'
import { useStorage } from '@alessiofrittoli/react-hooks'
type Locale = 'it' | 'en'
const storage = 'local' // or 'session'
const defaultLocale = 'it'
export const SomeComponent: React.FC = () => {
const [ userLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
return (
...
)
}
'use client'
import { useCallback } from 'react'
import { useStorage } from '@alessiofrittoli/react-hooks'
type Locale = 'it' | 'en'
const storage = 'local' // or 'session'
const defaultLocale = 'it'
export const LanguageSwitcher: React.FC = () => {
const [ userLocale, setUserLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
const clickHandler = useCallback( () => {
setUserLocale( 'en' )
}, [ setUserLocale ] )
return (
...
)
}
'use client'
import { useCallback } from 'react'
import { useStorage } from '@alessiofrittoli/react-hooks'
type Locale = 'it' | 'en'
const storage = 'local' // or 'session'
const defaultLocale = 'it'
export const LanguageSwitcher: React.FC = () => {
const [ userLocale, setUserLocale ] = useStorage<Locale>( 'user-locale', defaultLocale, storage )
const deleteHandler = useCallback( () => {
setUserLocale( null )
// or
setUserLocale( undefined )
// or
setUserLocale( '' )
}, [ setUserLocale ] )
return (
...
)
}
Shortcut React Hook for useStorage
.
Applies the same API Reference.
Shortcut React Hook for useStorage
.
Applies the same API Reference.
Get Document Media matches and listen for changes.
Parameters
Parameter | Type | Description |
---|---|---|
query |
string |
A string specifying the media query to parse into a MediaQueryList . |
Returns
Type: boolean
-
true
if the document currently matches the media query list. -
false
otherwise.
Usage
import { useMediaQuery } from '@alessiofrittoli/react-hooks'
const isDarkOS = useMediaQuery( '(prefers-color-scheme: dark)' )
Easily manage dark mode with full respect for user device preferences.
This hook is user-oriented and built to honor system-level color scheme settings:
- If the device prefers a dark color scheme, dark mode is automatically enabled on first load.
- If the user enables/disables dark mode via a web widget, the preference is stored in
localStorage
under the keydark-mode
. - If the device color scheme preference changes (e.g. via OS settings), that change takes precedence and is stored for future visits.
Parameters
Parameter | Type | Description |
---|---|---|
options |
UseDarkModeOptions |
(Optional) Configuration object for the hook. |
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. |
options.docClassNames |
[dark: string, light: string] |
(Optional) Array of class names to toggle on the <html> element, e.g. ['dark', 'light'] . |
Returns
Type: UseDarkModeOutput
An object containing utilities for managing dark mode:
-
isDarkMode
:boolean
— Whether dark mode is currently enabled. -
isDarkOS
:boolean
— Whether the user's system prefers dark mode. -
toggleDarkMode
:() => void
— Toggles dark mode and saves the preference. -
enableDarkMode
:() => void
— Enables dark mode and saves the preference. -
disableDarkMode
:() => void
— Disables dark mode and saves the preference.
Usage
'use client'
import { useDarkMode } from '@alessiofrittoli/react-hooks'
export const Component: React.FC = () => {
const { isDarkMode } = useDarkMode()
return (
<div>{ isDarkMode ? 'Dark mode enabled' : 'Dark mode disabled' }</div>
)
}
// Component.tsx
'use client'
import { useDarkMode } from '@alessiofrittoli/react-hooks'
export const Component: React.FC = () => {
const { isDarkMode } = useDarkMode( {
docClassNames: [ 'dark', 'light' ],
} )
return (
<div>{ isDarkMode ? 'Dark mode enabled' : 'Dark mode disabled' }</div>
)
}
/* style.css */
.light {
color-scheme: light;
}
.dark {
color-scheme: dark;
}
.light body
{
color : black;
background: white;
}
.dark body
{
color : white;
background: black;
}
'use client'
import { useDarkMode } from '@alessiofrittoli/react-hooks'
export const ThemeSwitcher: React.FC = () => {
const { isDarkMode, toggleDarkMode } = useDarkMode()
return (
<button onClick={ toggleDarkMode }>
{ isDarkMode ? '🌙' : '☀️' }
</button>
)
}
Browsers automatically apply colorization using:
<meta name='theme-color' media='(prefers-color-scheme: dark)' />
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.
To ensure consistency, useDarkMode
updates these meta tags dynamically based on the actual mode.
Just make sure to define both light
and dark
theme-color tags in your document:
<head>
<meta name='theme-color' media='(prefers-color-scheme: light)' content='lime'>
<meta name='theme-color' media='(prefers-color-scheme: dark)' content='aqua'>
</head>
Check if device is portrait oriented.
React State get updated when device orientation changes.
Returns
Type: boolean
-
true
if the device is portrait oriented. -
false
otherwise.
Usage
import { useIsPortrait } from '@alessiofrittoli/react-hooks'
const isLandscape = ! useIsPortrait()
Prevent Element overflow.
Parameters
Parameter | Type | Default | Description |
---|---|---|---|
target |
React.RefObject<HTMLElement|null> |
Document.documentElement |
(Optional) The React RefObject target HTMLElement. |
Returns
Type: [ () => void, () => void ]
A tuple with block and restore scroll callbacks.
Usage
import { useScrollBlock } from '@alessiofrittoli/react-hooks'
const [ blockScroll, restoreScroll ] = useScrollBlock()
const openPopUpHandler = useCallback( () => {
...
blockScroll()
}, [ blockScroll ] )
const closePopUpHandler = useCallback( () => {
...
restoreScroll()
}, [ restoreScroll ] )
...
const elementRef = useRef<HTMLDivElement>( null )
const [ blockScroll, restoreScroll ] = useScrollBlock( elementRef )
const scrollBlockHandler = useCallback( () => {
...
blockScroll()
}, [ blockScroll ] )
const scrollRestoreHandler = useCallback( () => {
...
restoreScroll()
}, [ restoreScroll ] )
...
Trap focus inside the given HTML Element.
This comes pretty handy when rendering a modal that shouldn't be closed without a user required action.
Parameters
Parameter | Type | Description |
---|---|---|
target |
React.RefObject<HTMLElement|null> |
The target HTMLElement React RefObject to trap focus within. |
If no target is given, you must provide the target HTMLElement when calling setFocusTrap . |
Returns
Type: readonly [ SetFocusTrap, RestoreFocusTrap ]
A tuple containing:
-
setFocusTrap
: A function to enable the focus trap. Optionally accept an HTMLElement as target. -
restoreFocusTrap
: A function to restore the previous focus state.
Usage
import { useFocusTrap } from '@alessiofrittoli/react-hooks'
const modalRef = useRef<HTMLDivElement>( null )
const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap( modalRef )
const modalOpenHandler = useCallback( () => {
if ( ! modalRef.current ) return
// ... open modal
setFocusTrap()
modalRef.current.focus() // focus the dialog so next tab will focus the next element inside the modal
}, [ setFocusTrap ] )
const modalCloseHandler = useCallback( () => {
// ... close modal
restoreFocusTrap() // cancel focus trap and restore focus to the last active element before enablig the focus trap
}, [ restoreFocusTrap ] )
import { useFocusTrap } from '@alessiofrittoli/react-hooks'
const modalRef = useRef<HTMLDivElement>( null )
const modal2Ref = useRef<HTMLDivElement>( null )
const [ setFocusTrap, restoreFocusTrap ] = useFocusTrap()
const modalOpenHandler = useCallback( () => {
if ( ! modalRef.current ) return
// ... open modal
setFocusTrap( modalRef.current )
modalRef.current.focus()
}, [ setFocusTrap ] )
const modal2OpenHandler = useCallback( () => {
if ( ! modal2Ref.current ) return
// ... open modal
setFocusTrap( modal2Ref.current )
modal2Ref.current.focus()
}, [ setFocusTrap ] )
Check if the given target Element is intersecting with an ancestor Element or with a top-level document's viewport.
Parameters
Parameter | Type | Description |
---|---|---|
target |
React.RefObject<Element|null> |
The React.RefObject of the target Element to observe. |
options |
UseInViewOptions |
(Optional) An object defining custom IntersectionObserver options. |
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. |
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. |
options.amount |
'all'|'some'|number|number[] |
(Optional) The intersecting target thresholds. |
Threshold can be set to: | ||
- all - 1 will be used. |
||
- some - 0.5 will be used. |
||
- number
|
||
- number[]
|
||
options.once |
boolean |
(Optional) By setting this to true the observer will be disconnected after the target Element enters the viewport. |
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 . |
options.enable |
boolean |
(Optional) Defines the initial observation activity. Use the returned setEnabled to update this state. Default: true . |
options.onIntersect |
OnIntersectStateHandler |
(Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
This callback is awaited before any state update. | ||
If an error is thrown the React State update won't be fired. | ||
useCallback to avoid unnecessary IntersectionObserver recreation. |
||
options.onEnter |
OnIntersectHandler |
(Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
This callback is awaited before any state update. | ||
If an error is thrown the React State update won't be fired. | ||
useCallback to avoid unnecessary IntersectionObserver recreation. |
||
options.onExit |
OnIntersectHandler |
(Optional) A custom callback executed when target element's visibility has crossed one or more thresholds. |
This callback is awaited before any state update. | ||
If an error is thrown the React State update won't be fired. | ||
useCallback to avoid unnecessary IntersectionObserver recreation. |
Returns
Type: UseInViewReturnType
An object containing:
-
inView
:boolean
- Indicates whether the target Element is in viewport or not. -
setInView
:React.Dispatch<React.SetStateAction<boolean>>
- A React Dispatch SetState action that allows custom state updates. -
enabled
:boolean
- Indicates whether the target Element is being observed or not. -
setEnabled
:React.Dispatch<React.SetStateAction<boolean>>
- A React Dispatch SetState action that allows to enable/disable observation when needed. -
observer
:IntersectionObserver | undefined
- TheIntersectionObserver
instance. It could beundefined
ifIntersectionObserver
is not available or observation is not enabled.
Usage
'use client'
import { useRef } from 'react'
import { useInView } from '@alessiofrittoli/react-hooks'
const UseInViewExample: React.FC = () => {
const targetRef = useRef<HTMLDivElement>( null )
const { inView } = useInView( ref )
return (
Array.from( Array( 6 ) ).map( ( value, index ) => (
<div
key={ index }
style={ {
height : '50vh',
border : '1px solid red',
display : 'flex',
alignItems : 'center',
justifyContent : 'center',
} }
>
<div
ref={ index === 2 ? targetRef : undefined }
style={ {
width : 150,
height : 150,
borderRadius : 12,
display : 'flex',
alignItems : 'center',
justifyContent : 'center',
background : inView ? '#51AF83' : '#201A1B',
color : inView ? '#201A1B' : '#FFFFFF',
} }
>{ index + 1 }</div>
</div>
) )
)
}
'use client'
import { useRef } from 'react'
import { useInView } from '@alessiofrittoli/react-hooks'
const OnceExample: React.FC = () => {
const targetRef = useRef<HTMLDivElement>( null )
const { inView } = useInView( targetRef, { once: true } )
useEffect( () => {
if ( ! inView ) return
console.count( 'Fired only once: element entered viewport.' )
}, [ inView ] )
return (
<div
ref={ targetRef }
style={ {
height : 200,
background : inView ? 'lime' : 'gray',
} }
/>
)
}
'use client'
import { useRef } from 'react'
import { useInView } from '@alessiofrittoli/react-hooks'
const OnDemandObservation: React.FC = () => {
const targetRef = useRef<HTMLDivElement>( null )
const {
inView, enabled, setEnabled
} = useInView( targetRef, { enable: false } )
return (
<div>
<button onClick={ () => setEnabled( prev => ! prev ) }>
{ enabled ? 'Disconnect observer' : 'Observe' }
</button>
<div
ref={ targetRef }
style={ {
height : 200,
marginTop : 50,
background : inView ? 'lime' : 'gray',
} }
/>
</div>
)
}
'use client'
import { useRef } from 'react'
import { useInView, type OnIntersectStateHandler } from '@alessiofrittoli/react-hooks'
const AsyncStartExample: React.FC = () => {
const targetRef = useRef<HTMLDivElement>( null )
const onIntersect = useCallback<OnIntersectStateHandler>( async ( { entry, isEntering } ) => {
if ( isEntering ) {
console.log( 'Delaying state update...' )
await new Promise( resolve => setTimeout( resolve, 1000 ) ) // Simulate delay
console.log( 'Async task completed. `inView` will now be updated.' )
return
}
console.log( 'Delaying state update...' )
await new Promise( resolve => setTimeout( resolve, 1000 ) ) // Simulate delay
console.log( 'Async task completed. `inView` will now be updated.' )
}, [] )
const { inView } = useInView( targetRef, { onIntersect } )
return (
<div
ref={ targetRef }
style={ {
height : 200,
background : inView ? 'lime' : 'gray',
} }
/>
)
}
'use client'
import { useRef } from 'react'
import { useInView, type OnIntersectHandler } from '@alessiofrittoli/react-hooks'
const AsyncStartExample: React.FC = () => {
const targetRef = useRef<HTMLDivElement>( null )
const onEnter = useCallback<OnIntersectHandler>( async ( { entry } ) => {
console.log( 'In viewport - ', entry )
}, [] )
const onExit = useCallback<OnIntersectHandler>( async ( { entry } ) => {
console.log( 'Exited viewport - ', entry )
}, [] )
const { inView } = useInView( targetRef, { onEnter, onExit } )
return (
<div
ref={ targetRef }
style={ {
height : 200,
background : inView ? 'lime' : 'gray',
} }
/>
)
}
Check if the React Hook or Component where this hook is executed is running in a browser environment.
This is pretty usefull to avoid hydration errors.
Returns
Type: boolean
-
true
if the React Hook or Component is running in a browser environment. -
false
otherwise.
Usage
'use client'
import { useIsClient } from '@alessiofrittoli/react-hooks'
export const ClientComponent: React.FC = () => {
const isClient = useIsClient()
return (
<div>Running { ! isClient ? 'server' : 'client' }-side</div>
)
}
Check if is first React Hook/Component render.
Returns
Type: boolean
-
true
at the mount time. -
false
otherwise.
Note that if the React Hook/Component has no state updates, useIsFirstRender
will always return true
.
Usage
'use client'
import { useIsFirstRender } from '@alessiofrittoli/react-hooks'
export const ClientComponent: React.FC = () => {
const isFirstRender = useIsFirstRender()
const [ counter, setCounter ] = useState( 0 )
useEffect( () => {
const intv = setInterval( () => {
setCounter( prev => prev + 1 )
}, 1000 )
return () => clearInterval( intv )
}, [] )
return (
<div>
{ isFirstRender ? 'First render' : 'Subsequent render' }
<hr />
{ counter }
</div>
)
}
Modified version of useEffect
that skips the first render.
Parameters
Parameter | Type | Description |
---|---|---|
effect |
React.EffectCallback |
Imperative function that can return a cleanup function. |
deps |
React.DependencyList |
If present, effect will only activate if the values in the list change. |
Usage
'use client'
import { useEffect, useState } from 'react'
import { useUpdateEffect } from '@alessiofrittoli/react-hooks'
export const ClientComponent: React.FC = () => {
const [ count, setCount ] = useState( 0 )
useEffect( () => {
const intv = setInterval( () => {
setCount( prev => prev + 1 )
}, 1000 )
return () => clearInterval( intv )
}, [] )
useEffect( () => {
console.log( 'useEffect', count ) // starts from 0
return () => {
console.log( 'useEffect - clean up', count ) // starts from 0
}
}, [ count ] )
useUpdateEffect( () => {
console.log( 'useUpdateEffect', count ) // starts from 1
return () => {
console.log( 'useUpdateEffect - clean up', count ) // starts from 1
}
}, [ count ] )
return (
<div>{ count }</div>
)
}
Get pagination informations based on the given options.
This hook memoize the returned result of the paginate
function imported from @alessiofrittoli/math-utils
.
See paginate
function Documentation for more information about it.
npm install
or using pnpm
pnpm i
Run the following command to test and build code for distribution.
pnpm build
warnings / errors check.
pnpm lint
Run all the defined test suites by running the following:
# Run tests and watch file changes.
pnpm test:watch
# Run tests in a CI environment.
pnpm test:ci
- See
package.json
file scripts for more info.
Run tests with coverage.
An HTTP server is then started to serve coverage files from ./coverage
folder.
test:coverage:serve
Contributions are truly welcome!
Please refer to the Contributing Doc for more information on how to start contributing to this project.
Help keep this project up to date with GitHub Sponsor.
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.
|
|