useElementSize
A hook to track an element's width and height
Loading...
Installation
npx shadcn@latest add @shadcnhooks/use-element-sizepnpm dlx shadcn@latest add @shadcnhooks/use-element-sizeyarn dlx shadcn@latest add @shadcnhooks/use-element-sizebun x shadcn@latest add @shadcnhooks/use-element-sizeCopy and paste the following code into your project.
import { isBrowser, isEqual, isFunction } from 'es-toolkit'
import { useRef } from 'react'
import { useUnmount } from '@/registry/hooks/use-unmount'
import type {
DependencyList,
EffectCallback,
RefObject,
useEffect,
useLayoutEffect,
} from 'react'
type TargetValue<T> = T | undefined | null
type TargetType = HTMLElement | Element | Window | Document
export type BasicTarget<T extends TargetType = Element> =
| (() => TargetValue<T>)
| TargetValue<T>
| RefObject<TargetValue<T>>
export function getTargetElement<T extends TargetType>(
target: BasicTarget<T>,
defaultElement?: T,
) {
if (!isBrowser) {
return undefined
}
if (!target) {
return defaultElement
}
let targetElement: TargetValue<T>
if (isFunction(target)) {
targetElement = target()
} else if ('current' in target) {
targetElement = target.current
} else {
targetElement = target
}
return targetElement
}
export function createEffectWithTarget(
useEffectType: typeof useEffect | typeof useLayoutEffect,
) {
/**
*
* @param effect
* @param deps
* @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom
*/
const useEffectWithTarget = (
effect: EffectCallback,
deps: DependencyList,
target: BasicTarget<any> | BasicTarget<any>[],
) => {
const hasInitRef = useRef(false)
const lastElementRef = useRef<(Element | null)[]>([])
const lastDepsRef = useRef<DependencyList>([])
const unLoadRef = useRef<any>(undefined)
useEffectType(() => {
const targets = Array.isArray(target) ? target : [target]
const els = targets.map((item) => getTargetElement(item))
// init run
if (!hasInitRef.current) {
hasInitRef.current = true
lastElementRef.current = els
lastDepsRef.current = deps
unLoadRef.current = effect()
return
}
if (
els.length !== lastElementRef.current.length ||
!isEqual(lastElementRef.current, els) ||
!isEqual(lastDepsRef.current, deps)
) {
unLoadRef.current?.()
lastElementRef.current = els
lastDepsRef.current = deps
unLoadRef.current = effect()
}
})
useUnmount(() => {
unLoadRef.current?.()
// for react-refresh
hasInitRef.current = false
})
}
return useEffectWithTarget
}import { useEffect } from 'react'
import { createEffectWithTarget } from '@/registry/lib/create-effect-with-target'
export const useEffectWithTarget = createEffectWithTarget(useEffect)import { useState } from 'react'
import { useEffectWithTarget } from '@/registry/hooks/use-effect-with-target'
import { getTargetElement } from '@/registry/lib/create-effect-with-target'
import type { BasicTarget } from '@/registry/lib/create-effect-with-target'
export interface ElementSize {
width: number
height: number
}
export interface UseElementSizeOptions {
/**
* ResizeObserver box model to use for measuring.
*
* @default 'content-box'
*/
box?: ResizeObserverBoxOptions
}
const defaultInitialSize: ElementSize = {
width: 0,
height: 0,
}
type ResizeObserverBoxSize =
| ResizeObserverSize
| ReadonlyArray<ResizeObserverSize>
function toBoxSizeArray(
boxSize: ResizeObserverBoxSize | undefined,
): readonly ResizeObserverSize[] {
if (!boxSize) {
return []
}
return Array.isArray(boxSize)
? boxSize
: ([boxSize] as readonly ResizeObserverSize[])
}
function getSizeFromEntry(
entry: ResizeObserverEntry,
box: ResizeObserverBoxOptions,
): ElementSize {
const boxSize =
box === 'border-box'
? entry.borderBoxSize
: box === 'device-pixel-content-box'
? entry.devicePixelContentBoxSize
: entry.contentBoxSize
const sizes = toBoxSizeArray(boxSize)
if (sizes.length > 0) {
return {
width: sizes.reduce((sum, size) => sum + size.inlineSize, 0),
height: sizes.reduce((sum, size) => sum + size.blockSize, 0),
}
}
return {
width: entry.contentRect.width,
height: entry.contentRect.height,
}
}
function isSvgElement(element: Element): element is SVGElement {
return typeof SVGElement !== 'undefined' && element instanceof SVGElement
}
export function useElementSize(
target: BasicTarget<Element>,
initialSize: ElementSize = defaultInitialSize,
options: UseElementSizeOptions = {},
): ElementSize {
const { box = 'content-box' } = options
const [size, setSize] = useState<ElementSize>(initialSize)
const updateSize = (nextSize: ElementSize) => {
setSize((currentSize) => {
if (
currentSize.width === nextSize.width &&
currentSize.height === nextSize.height
) {
return currentSize
}
return nextSize
})
}
useEffectWithTarget(
() => {
const element = getTargetElement(target)
if (!element) {
updateSize({ width: 0, height: 0 })
return
}
updateSize(initialSize)
if (typeof ResizeObserver === 'undefined') {
const rect = element.getBoundingClientRect()
updateSize({
width: rect.width,
height: rect.height,
})
return
}
const observer = new ResizeObserver((entries) => {
const entry = entries[0]
if (!entry) {
return
}
if (isSvgElement(element)) {
const rect = element.getBoundingClientRect()
updateSize({
width: rect.width,
height: rect.height,
})
return
}
updateSize(getSizeFromEntry(entry, box))
})
try {
observer.observe(element, { box })
} catch {
observer.observe(element)
}
return () => {
observer.disconnect()
}
},
[box, initialSize.width, initialSize.height],
target,
)
return size
}API
export interface ElementSize {
width: number
height: number
}
export interface UseElementSizeOptions {
/**
* ResizeObserver box model to use for measuring.
*
* @default 'content-box'
*/
box?: ResizeObserverBoxOptions
}
/**
* A hook to track an element's width and height.
*
* @param target - The target element to observe
* @param initialSize - Initial width and height
* @param options - Measurement options
* @returns Current element size
*/
export function useElementSize(
target: BasicTarget<Element>,
initialSize?: ElementSize,
options?: UseElementSizeOptions,
): ElementSizeCredits
Last updated on