From 03e5993e2a56f2e721ae502a2e022dab4208e923 Mon Sep 17 00:00:00 2001 From: pawn1023 Date: Tue, 20 Oct 2026 00:14:30 +0900 Subject: [PATCH] fix: restore useThrottle updates after StrictMode cleanup --- src/useThrottle.ts | 6 +++++- tests/useThrottle.test.ts | 40 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/useThrottle.ts b/src/useThrottle.ts index b231ba626d..854bec3d18 100644 --- a/src/useThrottle.ts +++ b/src/useThrottle.ts @@ -27,7 +27,11 @@ const useThrottle = (value: T, ms: number = 200) => { }, [value]); useUnmount(() => { - timeout.current && clearTimeout(timeout.current); + if (timeout.current) { + clearTimeout(timeout.current); + } + timeout.current = undefined; + hasNextValue.current = false; }); return state; diff --git a/tests/useThrottle.test.ts b/tests/useThrottle.test.ts index 6b4128a0d9..2bb762e239 100644 --- a/tests/useThrottle.test.ts +++ b/tests/useThrottle.test.ts @@ -1,4 +1,5 @@ -import { renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react-hooks'; +import * as useUnmountModule from '../src/useUnmount'; import useThrottle from '../src/useThrottle'; describe('useThrottle', () => { @@ -63,6 +64,43 @@ describe('useThrottle', () => { expect(hook.result.current).toBe(0); }); + it('resumes updates after a StrictMode-like effect cleanup', () => { + // React 17 is used by this test suite, so manually replay the cleanup + // that React 18 StrictMode invokes before restarting mounted effects. + const originalUseUnmount = useUnmountModule.default; + let replayCleanup: (() => any) | undefined; + const spy = jest.spyOn(useUnmountModule, 'default').mockImplementation((fn) => { + replayCleanup = fn; + originalUseUnmount(fn); + }); + + try { + const hook = renderHook((value) => useThrottle(value, 100), { initialProps: 0 }); + expect(jest.getTimerCount()).toBe(1); + + act(() => { + if (!replayCleanup) { + throw new Error('Unmount cleanup was not registered'); + } + replayCleanup(); + }); + expect(jest.getTimerCount()).toBe(0); + + hook.rerender(1); + expect(hook.result.current).toBe(1); + expect(jest.getTimerCount()).toBe(1); + + hook.rerender(2); + act(() => { + jest.advanceTimersByTime(100); + }); + expect(hook.result.current).toBe(2); + hook.unmount(); + } finally { + spy.mockRestore(); + } + }); + it('should cancel timeout on unmount', () => { const hook = renderHook((props) => useThrottle(props, 100), { initialProps: 0 }); expect(hook.result.current).toBe(0);