useEffectWithTarget
A hook to create an effect with a target element that re-runs when the target or dependencies change
Installation
npx shadcn@latest add @hooks/use-effect-with-targetpnpm dlx shadcn@latest add @hooks/use-effect-with-targetyarn dlx shadcn@latest add @hooks/use-effect-with-targetbun x shadcn@latest add @hooks/use-effect-with-targetCopy 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)Usage
useEffectWithTarget is similar to useEffect, but it accepts a target parameter. The effect will re-run when either the dependencies or the target element changes.
Basic Usage
import { useEffectWithTarget } from '@/hooks/use-effect-with-target'
import { useRef } from 'react'
function MyComponent() {
const divRef = useRef<HTMLDivElement>(null)
useEffectWithTarget(
() => {
const element = divRef.current
if (element) {
// Do something with the element
console.log('Element mounted:', element)
}
return () => {
// Cleanup
console.log('Element unmounted')
}
},
[],
divRef,
)
return <div ref={divRef}>Hello</div>
}With DOM Element
import { useEffectWithTarget } from '@/hooks/use-effect-with-target'
function MyComponent() {
const element = document.getElementById('my-element')
useEffectWithTarget(
() => {
if (element) {
// Do something with the element
}
},
[],
element,
)
return <div id='my-element'>Hello</div>
}With Function Target
import { useEffectWithTarget } from '@/hooks/use-effect-with-target'
function MyComponent() {
useEffectWithTarget(
() => {
const element = document.querySelector('.my-class')
if (element) {
// Do something with the element
}
},
[],
() => document.querySelector('.my-class'),
)
return <div className='my-class'>Hello</div>
}With Multiple Targets
import { useEffectWithTarget } from '@/hooks/use-effect-with-target'
import { useRef } from 'react'
function MyComponent() {
const ref1 = useRef<HTMLDivElement>(null)
const ref2 = useRef<HTMLDivElement>(null)
useEffectWithTarget(
() => {
// Effect runs when either ref1 or ref2 changes
console.log('Targets changed')
},
[],
[ref1, ref2],
)
return (
<>
<div ref={ref1}>First</div>
<div ref={ref2}>Second</div>
</>
)
}With Dependencies
import { useEffectWithTarget } from '@/hooks/use-effect-with-target'
import { useRef, useState } from 'react'
function MyComponent() {
const [count, setCount] = useState(0)
const divRef = useRef<HTMLDivElement>(null)
useEffectWithTarget(
() => {
// Effect runs when count changes or when divRef.current changes
console.log('Count:', count)
},
[count],
divRef,
)
return (
<div ref={divRef}>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}API
type TargetValue<T> = T | undefined | null
type TargetType = HTMLElement | Element | Window | Document
type BasicTarget<T extends TargetType = Element> =
| (() => TargetValue<T>)
| TargetValue<T>
| RefObject<TargetValue<T>>
/**
* A hook to create an effect with a target element
* @param effect - The effect callback function
* @param deps - The dependency array (same as useEffect)
* @param target - The target element(s). Can be a DOM element, ref object, function that returns an element, or an array of these
* @returns void
*/
export const useEffectWithTarget: (
effect: EffectCallback,
deps: DependencyList,
target: BasicTarget<any> | BasicTarget<any>[],
) => voidCredits
Last updated on