From c677df2e2d1600a4402f43274851e62eee9eb9e5 Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:53:59 -0600 Subject: [PATCH] feat: add useTimeout --- CHANGELOG.md | 6 +++ jsr.json | 2 +- package.json | 2 +- src/__tests__/hooks/state/useTimeout.test.ts | 53 ++++++++++++++++++++ src/hooks/dom/useHideOnScrollDown.ts | 4 +- src/hooks/state/index.ts | 1 + src/hooks/state/useTimeout.ts | 48 ++++++++++++++++++ 7 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/hooks/state/useTimeout.test.ts create mode 100644 src/hooks/state/useTimeout.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 181bccc..e2829ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @zl-asica/react +## 0.3.13 + +### Patch Changes + +- fix useHideOnScrollDown type, add useTimeout + ## 0.3.12 ### Patch Changes diff --git a/jsr.json b/jsr.json index dc65fe2..d39b610 100644 --- a/jsr.json +++ b/jsr.json @@ -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", diff --git a/package.json b/package.json index e6d2b44..287c2d4 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/__tests__/hooks/state/useTimeout.test.ts b/src/__tests__/hooks/state/useTimeout.test.ts new file mode 100644 index 0000000..19464d0 --- /dev/null +++ b/src/__tests__/hooks/state/useTimeout.test.ts @@ -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) + }) +}) diff --git a/src/hooks/dom/useHideOnScrollDown.ts b/src/hooks/dom/useHideOnScrollDown.ts index a85623d..0afd35d 100644 --- a/src/hooks/dom/useHideOnScrollDown.ts +++ b/src/hooks/dom/useHideOnScrollDown.ts @@ -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} [targetRef] - The reference to the target element (e.g., header). If not provided, defaults to `null` (uses `threshold`). + * @param {React.RefObject} [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. * @@ -37,7 +37,7 @@ import { useEventListener } from './useEventListener' * ) */ export const useHideOnScrollDown = ( - targetRef?: RefObject, + targetRef?: RefObject, threshold: number = 50, ): boolean => { const [isVisible, setIsVisible] = useState(true) diff --git a/src/hooks/state/index.ts b/src/hooks/state/index.ts index dfd9b47..cada675 100644 --- a/src/hooks/state/index.ts +++ b/src/hooks/state/index.ts @@ -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' diff --git a/src/hooks/state/useTimeout.ts b/src/hooks/state/useTimeout.ts new file mode 100644 index 0000000..52fc8c2 --- /dev/null +++ b/src/hooks/state/useTimeout.ts @@ -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
{isVisible ? 'Visible' : 'Hidden'}
+ * } + * ``` + */ +export const useTimeout = (callback: (() => void) | void, delay: number = 0): void => { + const timeoutRef = useRef(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]) +}