From 4423e555992c0caa21522148838ba64635d3c790 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Fri, 26 Jul 2024 09:23:42 +0200 Subject: [PATCH] fix: handle unmount in useDebouncedCallback --- src/__tests__/useDebouncedCallback.test.tsx | 22 +++++++++++++++++++-- src/hooks/useDebouncedCallback.ts | 9 ++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/__tests__/useDebouncedCallback.test.tsx b/src/__tests__/useDebouncedCallback.test.tsx index 5952431..48b441f 100644 --- a/src/__tests__/useDebouncedCallback.test.tsx +++ b/src/__tests__/useDebouncedCallback.test.tsx @@ -1,11 +1,16 @@ import { renderHook } from "@testing-library/react"; -import { beforeEach } from "vitest"; +import { afterEach, beforeAll } from "vitest"; import { useDebouncedCallback } from "../hooks/useDebouncedCallback"; -beforeEach(() => { +beforeAll(() => { vi.useFakeTimers(); }); +afterEach(() => { + // Should be no pending timers after each test + expect(vi.getTimerCount()).toBe(0); +}); + test("should call the callback after the delay", async () => { const cb = vi.fn(); const { result } = renderHook(() => useDebouncedCallback(cb, 500)); @@ -87,6 +92,7 @@ test("should handle leading option", async () => { vi.advanceTimersToNextTimer(); result.current("c"); expect(cb).toHaveBeenCalledWith("c"); + vi.advanceTimersToNextTimer(); }); test("should handle both leading and trailing option", async () => { @@ -106,6 +112,18 @@ test("should handle both leading and trailing option", async () => { expect(cb).toHaveBeenCalledWith("b"); }); +test("should call stop pending callbacks on unmount", async () => { + const cb = vi.fn(); + const { result, unmount } = renderHook(() => useDebouncedCallback(cb, 500)); + + result.current(); + unmount(); + + // After unmounting, the callback should not be called and there should be no pending timers + expect(vi.getTimerCount()).toBe(0); + expect(cb).not.toHaveBeenCalled(); +}); + test("should infer the correct callback signature", async () => { const cb = (value: string, count: number, opts: { input: string }) => {}; const { result } = renderHook(() => useDebouncedCallback(cb, 500)); diff --git a/src/hooks/useDebouncedCallback.ts b/src/hooks/useDebouncedCallback.ts index 8c66b23..0212ac5 100644 --- a/src/hooks/useDebouncedCallback.ts +++ b/src/hooks/useDebouncedCallback.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef } from "react"; type DebounceOptions = { /** @@ -60,6 +60,13 @@ export function useDebouncedCallback< // This ensures that the user doesn't accidentally recreate the debounced function. cb.current = func; + useEffect(() => { + return () => { + // Clear any pending timeouts when the hook unmounts + if (timeout.current) clearTimeout(timeout.current); + }; + }, []); + return useMemo(() => { let currentArgs: Parameters;