diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 639583d..e4ca489 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -11,3 +11,4 @@ export * from "./useTimeout"; export * from "./useWindowSize"; export * from "./useVisibilityChange"; export * from "./useObjectState"; +export * from "./useDebounce"; diff --git a/src/hooks/useDebounce/index.ts b/src/hooks/useDebounce/index.ts new file mode 100644 index 0000000..c94f3e1 --- /dev/null +++ b/src/hooks/useDebounce/index.ts @@ -0,0 +1 @@ +export * from "./useDebounce"; diff --git a/src/hooks/useDebounce/useDebounce.test.tsx b/src/hooks/useDebounce/useDebounce.test.tsx new file mode 100644 index 0000000..a14324f --- /dev/null +++ b/src/hooks/useDebounce/useDebounce.test.tsx @@ -0,0 +1,100 @@ +import { renderHook, act } from "@testing-library/react-hooks"; +import useDebounce from "./useDebounce"; +import { describe, it, expect, vi, beforeAll, afterAll } from "vitest"; + +describe("useDebounce", () => { + beforeAll(() => { + vi.useFakeTimers(); + }); + + afterAll(() => { + vi.clearAllTimers(); + }); + + it("should return the initial value immediately", () => { + // act + const { result } = renderHook(() => useDebounce("initial", 500)); + + // assert + expect(result.current).toBe("initial"); + }); + + it("should debounce the value after the specified delay", async () => { + // act + const { result, rerender } = renderHook( + ({ value, delay }) => useDebounce(value, delay), + { + initialProps: { value: "initial", delay: 500 }, + } + ); + + // act + expect(result.current).toBe("initial"); + + rerender({ value: "updated", delay: 500 }); + + expect(result.current).toBe("initial"); + + act(() => { + vi.advanceTimersByTime(500); + }); + + expect(result.current).toBe("updated"); + }); + + it("should reset the debounce timer if the value changes before the delay", () => { + // act + const { result, rerender } = renderHook( + ({ value, delay }) => useDebounce(value, delay), + { + initialProps: { value: "initial", delay: 500 }, + } + ); + + // assert + expect(result.current).toBe("initial"); + + rerender({ value: "updated1", delay: 500 }); + + act(() => { + vi.advanceTimersByTime(300); + }); + + expect(result.current).toBe("initial"); + + rerender({ value: "updated2", delay: 500 }); + + act(() => { + vi.advanceTimersByTime(500); + }); + + expect(result.current).toBe("updated2"); + }); + + it("should handle delay changes correctly", () => { + // act + const { result, rerender } = renderHook( + ({ value, delay }) => useDebounce(value, delay), + { + initialProps: { value: "initial", delay: 500 }, + } + ); + + // assert + expect(result.current).toBe("initial"); + + rerender({ value: "updated", delay: 1000 }); + + act(() => { + vi.advanceTimersByTime(500); + }); + + expect(result.current).toBe("initial"); + + act(() => { + vi.advanceTimersByTime(500); + }); + + expect(result.current).toBe("updated"); + }); +}); diff --git a/src/hooks/useDebounce/useDebounce.tsx b/src/hooks/useDebounce/useDebounce.tsx new file mode 100644 index 0000000..1fc6770 --- /dev/null +++ b/src/hooks/useDebounce/useDebounce.tsx @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; + +export default function useDebounce(value: unknown, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const id = window.setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + window.clearTimeout(id); + }; + }, [value, delay]); + + return debouncedValue; +}