Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ coverage

# Local Netlify folder
.netlify

# IDE stuff
/.idea
22 changes: 12 additions & 10 deletions packages/usehooks-ts/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "usehooks-ts",
"private": false,
"version": "3.1.1",
"version": "3.2.0",
"description": "React hook library, ready to use, written in Typescript.",
"author": "Julien CARON <[email protected]>",
"homepage": "https://usehooks-ts.com",
Expand Down Expand Up @@ -38,25 +38,27 @@
},
"devDependencies": {
"@juggle/resize-observer": "^3.4.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^20.11.19",
"@types/react": "18.2.73",
"@types/react": "19.2.10",
"eslint-config-custom": "workspace:*",
"eslint-plugin-jsdoc": "^48.1.0",
"eslint-plugin-tree-shaking": "^1.12.1",
"jsdom": "^24.0.0",
"react": "18.2.0",
"tsup": "^8.0.2",
"typescript": "^5.3.3",
"vitest": "^1.3.1"
"jsdom": "^27.4.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"dependencies": {
"lodash.debounce": "^4.0.8"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
"react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
},
"engines": {
"node": ">=16.15.0"
Expand Down
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from './useScreen'
export * from './useScript'
export * from './useScrollLock'
export * from './useSessionStorage'
export * from './useSize'
export * from './useStep'
export * from './useTernaryDarkMode'
export * from './useTimeout'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function useDebounceCallback<T extends (...args: any) => ReturnType<T>>(
delay = 500,
options?: DebounceOptions,
): DebouncedState<T> {
const debouncedFunc = useRef<ReturnType<typeof debounce>>()
const debouncedFunc = useRef<ReturnType<typeof debounce>>(null)

useUnmount(() => {
if (debouncedFunc.current) {
Expand Down
17 changes: 10 additions & 7 deletions packages/usehooks-ts/src/useEventListener/useEventListener.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useEffect, useRef } from 'react'

import type { RefObject } from 'react'
import type { Ref } from 'react'

import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect'

// MediaQueryList Event based useEventListener interface
function useEventListener<K extends keyof MediaQueryListEventMap>(
eventName: K,
handler: (event: MediaQueryListEventMap[K]) => void,
element: RefObject<MediaQueryList>,
element: Ref<MediaQueryList>,
options?: boolean | AddEventListenerOptions,
): void

Expand All @@ -31,15 +31,15 @@ function useEventListener<
handler:
| ((event: HTMLElementEventMap[K]) => void)
| ((event: SVGElementEventMap[K]) => void),
element: RefObject<T>,
element: Ref<T>,
options?: boolean | AddEventListenerOptions,
): void

// Document Event based useEventListener interface
function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (event: DocumentEventMap[K]) => void,
element: RefObject<Document>,
element: Ref<Document>,
options?: boolean | AddEventListenerOptions,
): void

Expand All @@ -51,7 +51,7 @@ function useEventListener<K extends keyof DocumentEventMap>(
* @template T - The type of the DOM element (default is `HTMLElement`).
* @param {KW | KH | KM} eventName - The name of the event to listen for.
* @param {(event: WindowEventMap[KW] | HTMLElementEventMap[KH] | SVGElementEventMap[KH] | MediaQueryListEventMap[KM] | Event) => void} handler - The event handler function.
* @param {RefObject<T>} [element] - The DOM element or media query list to attach the event listener to (optional).
* @param {Ref<T>} [element] - The DOM element or media query list to attach the event listener to (optional).
* @param {boolean | AddEventListenerOptions} [options] - An options object that specifies characteristics about the event listener (optional).
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-event-listener)
Expand Down Expand Up @@ -88,7 +88,7 @@ function useEventListener<
| MediaQueryListEventMap[KM]
| Event,
) => void,
element?: RefObject<T>,
element?: Ref<T>,
options?: boolean | AddEventListenerOptions,
) {
// Create a ref that stores handler
Expand All @@ -100,7 +100,10 @@ function useEventListener<

useEffect(() => {
// Define the listening target
const targetElement: T | Window = element?.current ?? window
const targetElement: T | Window =
element && 'current' in element && element.current
? element.current
: window

if (!(targetElement && targetElement.addEventListener)) return

Expand Down
4 changes: 2 additions & 2 deletions packages/usehooks-ts/src/useHover/useHover.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react'

import type { RefObject } from 'react'
import type { Ref } from 'react'

import { useEventListener } from '../useEventListener'

Expand All @@ -19,7 +19,7 @@ import { useEventListener } from '../useEventListener'
* ```
*/
export function useHover<T extends HTMLElement = HTMLElement>(
elementRef: RefObject<T>,
elementRef: Ref<T>,
): boolean {
const [value, setValue] = useState<boolean>(false)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function useIntersectionObserver({
entry: undefined,
}))

const callbackRef = useRef<UseIntersectionObserverOptions['onChange']>()
const callbackRef = useRef<UseIntersectionObserverOptions['onChange']>(undefined)

callbackRef.current = onChange

Expand Down
13 changes: 6 additions & 7 deletions packages/usehooks-ts/src/useOnClickOutside/useOnClickOutside.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RefObject } from 'react'
import type { Ref } from 'react'

import { useEventListener } from '../useEventListener'
import { refContains } from '../utils'

/** Supported event types. */
type EventType =
Expand All @@ -14,7 +15,7 @@ type EventType =
/**
* Custom hook that handles clicks outside a specified element.
* @template T - The type of the element's reference.
* @param {RefObject<T> | RefObject<T>[]} ref - The React ref object(s) representing the element(s) to watch for outside clicks.
* @param {Ref<T> | Ref<T>[]} ref - The React ref object(s) representing the element(s) to watch for outside clicks.
* @param {(event: MouseEvent | TouchEvent | FocusEvent) => void} handler - The callback function to be executed when a click outside the element occurs.
* @param {EventType} [eventType] - The mouse event type to listen for (optional, default is 'mousedown').
* @param {?AddEventListenerOptions} [eventListenerOptions] - The options object to be passed to the `addEventListener` method (optional).
Expand All @@ -30,7 +31,7 @@ type EventType =
* ```
*/
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject<T> | RefObject<T>[],
ref: Ref<T> | Ref<T>[],
handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
eventType: EventType = 'mousedown',
eventListenerOptions: AddEventListenerOptions = {},
Expand All @@ -46,10 +47,8 @@ export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
}

const isOutside = Array.isArray(ref)
? ref
.filter(r => Boolean(r.current))
.every(r => r.current && !r.current.contains(target))
: ref.current && !ref.current.contains(target)
? ref.every(r => !refContains({ ref: r, target }))
: !refContains({ ref, target })

if (isOutside) {
handler(event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { renderHook } from '@testing-library/react'
import { useResizeObserver } from './useResizeObserver'

describe('useResizeObserver()', () => {
beforeEach(() => {
beforeAll(() => {
// Mock the ResizeObserver
window.ResizeObserver = ResizeObserver
vi.stubGlobal('ResizeObserver', ResizeObserver)
})

afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, useRef, useState } from 'react'

import type { RefObject } from 'react'
import type { Ref } from 'react'

import { useIsMounted } from '../useIsMounted'
import { refHasCurrent } from '../utils'

/** The size of the observed element. */
type Size = {
Expand All @@ -15,7 +16,7 @@ type Size = {
/** The options for the ResizeObserver. */
type UseResizeObserverOptions<T extends HTMLElement = HTMLElement> = {
/** The ref of the element to observe. */
ref: RefObject<T>
ref: Ref<T>
/**
* When using `onResize`, the hook doesn't re-render on element size changes; it delegates handling to the provided callback.
* @default undefined
Expand Down Expand Up @@ -62,7 +63,7 @@ export function useResizeObserver<T extends HTMLElement = HTMLElement>(
onResize.current = options.onResize

useEffect(() => {
if (!ref.current) return
if (!refHasCurrent(ref)) return

if (typeof window === 'undefined' || !('ResizeObserver' in window)) return

Expand Down
22 changes: 22 additions & 0 deletions packages/usehooks-ts/src/useSize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useRef, useState } from 'react'
import { useResizeObserver } from '../useResizeObserver'
import { useDebounceCallback } from '../useDebounceCallback'

export const useSize = (props?: {delay: number}) => {
const delay = props?.delay ?? 200

const ref = useRef<HTMLDivElement>(null)
const [{ height, width }, setSize] = useState<{
height?: number
width?: number
}>({ height: 0, width: 0 })

const onResize = useDebounceCallback(setSize, delay)

useResizeObserver({
onResize,
ref,
})

return { height, ref, width }
}
64 changes: 64 additions & 0 deletions packages/usehooks-ts/src/useSize/useSize.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useSize } from './index'

export default function Component() {
const { ref, width, height } = useSize({ frequency: 200 })

return (
<div
ref={ref}
style={{
border: '2px solid palevioletred',
borderRadius: '4px',
padding: '20px',
width: '100%',
resize: 'both',
overflow: 'auto',
maxWidth: '100%',
}}
>
<h3>Resize me!</h3>
<p>
Width: <strong>{width}px</strong>
</p>
<p>
Height: <strong>{height}px</strong>
</p>
<p style={{ marginTop: '16px', fontSize: '14px', color: '#666' }}>
Drag the bottom-right corner to resize this container. Updates are
debounced by 200ms.
</p>
</div>
)
}

export function CustomFrequency() {
const { ref, width, height } = useSize({ frequency: 500 })

return (
<div
ref={ref}
style={{
border: '2px solid mediumseagreen',
borderRadius: '4px',
padding: '20px',
width: '100%',
resize: 'both',
overflow: 'auto',
maxWidth: '100%',
marginTop: '20px',
}}
>
<h3>Custom debounce frequency (500ms)</h3>
<p>
Width: <strong>{width}px</strong>
</p>
<p>
Height: <strong>{height}px</strong>
</p>
<p style={{ marginTop: '16px', fontSize: '14px', color: '#666' }}>
This example uses a longer debounce delay of 500ms for less frequent
updates.
</p>
</div>
)
}
35 changes: 35 additions & 0 deletions packages/usehooks-ts/src/useSize/useSize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
A React hook that tracks the dimensions (width and height) of an element with debounced updates.

This hook combines `useResizeObserver` and `useDebounceCallback` to provide an ergonomic way to track element size changes without triggering excessive re-renders.

### Parameters

- `frequency`: The debounce delay in milliseconds. Controls how often size updates trigger re-renders. (default is `200`)

### Returns

An object containing:

- `ref`: A React ref that should be attached to the element you want to observe.
- `width`: The current width of the element in pixels.
- `height`: The current height of the element in pixels.

### Features

- Automatically tracks element size changes using ResizeObserver
- Debounces updates to prevent excessive re-renders during continuous resize events
- Provides a simple ref-based API for easy integration
- TypeScript support with proper type inference

### Use Cases

- Responsive layouts that need to adapt based on container size
- Charts and visualizations that need to resize with their container
- Virtual scrolling implementations
- Dynamic font sizing based on container dimensions
- Any UI component that needs to respond to size changes

Related hooks:

- [`useResizeObserver()`](/react-hook/use-resize-observer)
- [`useDebounceCallback()`](/react-hook/use-debounce-callback)
Loading
Loading