Skip to content

Commit

Permalink
feat: add useTimeout
Browse files Browse the repository at this point in the history
  • Loading branch information
ZL-Asica committed Feb 8, 2025
1 parent e18738c commit c677df2
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @zl-asica/react

## 0.3.13

### Patch Changes

- fix useHideOnScrollDown type, add useTimeout

## 0.3.12

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion jsr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zl-asica/react",
"version": "0.3.12",
"version": "0.3.13",
"license": "MIT",
"exports": "./src/index.ts",
"importMap": "./import_map.json",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zl-asica/react",
"type": "module",
"version": "0.3.12",
"version": "0.3.13",
"packageManager": "pnpm@9.15.4",
"description": "A library of reusable React hooks, components, and utilities built by ZL Asica.",
"author": {
Expand Down
53 changes: 53 additions & 0 deletions src/__tests__/hooks/state/useTimeout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useTimeout } from '@/hooks/state'
import { renderHook } from '@testing-library/react'

vi.useFakeTimers()

describe('useTimeout', () => {
it('should call callback after the delay', () => {
const callback = vi.fn()
renderHook(() => useTimeout(callback, 1000))

vi.advanceTimersByTime(1000)
expect(callback).toHaveBeenCalledTimes(1)
})

it('should clear timeout on unmount', () => {
const callback = vi.fn()
const { unmount } = renderHook(() => useTimeout(callback, 1000))

unmount()
vi.advanceTimersByTime(1000)
expect(callback).not.toHaveBeenCalled()
})

it('should reset timeout if delay changes', () => {
const callback = vi.fn()
const { rerender } = renderHook(({ delay }) => useTimeout(callback, delay), {
initialProps: { delay: 1000 },
})

vi.advanceTimersByTime(500)
rerender({ delay: 2000 }) // Change delay

vi.advanceTimersByTime(1500) // Total elapsed 2000ms (500ms + 1500ms)
expect(callback).not.toHaveBeenCalled() // Callback should not run because timeout reset

vi.advanceTimersByTime(500) // 2000ms reached
expect(callback).toHaveBeenCalledTimes(1)
})

it('should run callback immediately if delay is 0', () => {
const callback = vi.fn()
renderHook(() => useTimeout(callback, 0))

expect(callback).toHaveBeenCalledTimes(1)
})

it('should run callback only once if delay is negative', () => {
const callback = vi.fn()
renderHook(() => useTimeout(callback, -1000))

expect(callback).toHaveBeenCalledTimes(1)
})
})
4 changes: 2 additions & 2 deletions src/hooks/dom/useHideOnScrollDown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useEventListener } from './useEventListener'
* Custom hook to toggle visibility on scroll.
* It hides the element when scrolling down and shows it when scrolling up.
*
* @param {React.RefObject<HTMLElement>} [targetRef] - The reference to the target element (e.g., header). If not provided, defaults to `null` (uses `threshold`).
* @param {React.RefObject<HTMLElement | null>} [targetRef] - The reference to the target element (e.g., header). If not provided, defaults to `null` (uses `threshold`).
* @param {number} [threshold] - The scroll position threshold before hiding (used if `targetRef` is not provided).
* @returns {boolean} - Whether the element should be visible.
*
Expand All @@ -37,7 +37,7 @@ import { useEventListener } from './useEventListener'
* )
*/
export const useHideOnScrollDown = (
targetRef?: RefObject<HTMLElement>,
targetRef?: RefObject<HTMLElement | null>,
threshold: number = 50,
): boolean => {
const [isVisible, setIsVisible] = useState(true)
Expand Down
1 change: 1 addition & 0 deletions src/hooks/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export { useDebouncedCallback } from './useDebouncedCallback'
export { useLocalStorage } from './useLocalStorage'
export { useSessionStorage } from './useSessionStorage'
export { useThrottle } from './useThrottle'
export { useTimeout } from './useTimeout'
export { useToggle } from './useToggle'
48 changes: 48 additions & 0 deletions src/hooks/state/useTimeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client'

import { useEffect, useRef } from 'react'

/**
* Hook that runs a function after a specified delay.
* The timeout resets if the dependencies change.
*
* @param {(() => void) | void} callback - The function to execute after the timeout. Can be a function or a direct callable reference.
* @param {number} [delay] - The delay in milliseconds. Defaults to 0.
*
* @example
* ```tsx
* const MyComponent = () => {
* const [isVisible, setIsVisible] = useState(true)
*
* useTimeout(() => setIsVisible(false), 1000)
* // OR
* useTimeout(setIsVisible.bind(null, false), 1000)
*
* return <div>{isVisible ? 'Visible' : 'Hidden'}</div>
* }
* ```
*/
export const useTimeout = (callback: (() => void) | void, delay: number = 0): void => {
const timeoutRef = useRef<NodeJS.Timeout | null>(null)

useEffect(() => {
if (delay <= 0) {
if (callback instanceof Function) {
callback()
}
return
}

timeoutRef.current = setTimeout(() => {
if (callback instanceof Function) {
callback()
}
}, delay)

return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [callback, delay])
}

0 comments on commit c677df2

Please sign in to comment.