From da112e0040b8046c06f328005d8872aabaa9b8b5 Mon Sep 17 00:00:00 2001 From: Nik Date: Sun, 28 Jul 2024 12:52:28 +0100 Subject: [PATCH] Fix isPending state --- CHANGELOG.md | 4 ++ README.md | 1 + package.json | 2 +- src/useDebounce.ts | 4 ++ src/useDebouncedCallback.ts | 8 ++-- test/useDebounce.test.tsx | 93 +++++++++++++++++++++++++++++++++++-- 6 files changed, 104 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 138b847..9a4e609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 10.0.2 + +- Fixed: `isPending` does not reset the state if the tracked value hasn't changed.. See https://github.com/xnimorz/use-debounce/issues/178 + ## 10.0.1 - Fixed flush method return args, thanks to [@h](https://github.com/h) diff --git a/README.md b/README.md index f6aab47..d68a898 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ https://github.com/xnimorz/use-debounce/blob/master/CHANGELOG.md ## Simple values debouncing According to https://twitter.com/dan_abramov/status/1060729512227467264 +WebArchive link: https://web.archive.org/web/20210828073432/https://twitter.com/dan_abramov/status/1060729512227467264 ```javascript import React, { useState } from 'react'; diff --git a/package.json b/package.json index c1a97b7..658b42a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "use-debounce", - "version": "10.0.1", + "version": "10.0.2", "description": "Debounce hook for react", "source": "src/index.ts", "main": "dist/index.js", diff --git a/src/useDebounce.ts b/src/useDebounce.ts index 6680ea0..fdf139e 100644 --- a/src/useDebounce.ts +++ b/src/useDebounce.ts @@ -34,5 +34,9 @@ export default function useDebounce( previousValue.current = value; } + if (eq(state as T, value)) { + debounced.cancel(); + } + return [state as T, debounced]; } diff --git a/src/useDebouncedCallback.ts b/src/useDebouncedCallback.ts index dd92325..2d75b8c 100644 --- a/src/useDebouncedCallback.ts +++ b/src/useDebouncedCallback.ts @@ -126,9 +126,9 @@ export default function useDebouncedCallback< // Always keep the latest version of debounce callback, with no wait time. funcRef.current = func; - const isClientSize = typeof window !== 'undefined'; + const isClientSide = typeof window !== 'undefined'; // Bypass `requestAnimationFrame` by explicitly setting `wait=0`. - const useRAF = !wait && wait !== 0 && isClientSize; + const useRAF = !wait && wait !== 0 && isClientSide; if (typeof func !== 'function') { throw new TypeError('Expected a function'); @@ -229,7 +229,7 @@ export default function useDebouncedCallback< }; const func: DebouncedState = (...args: Parameters): ReturnType => { - if (!isClientSize && !debounceOnServer) { + if (!isClientSide && !debounceOnServer) { return; } const time = Date.now(); @@ -290,7 +290,7 @@ export default function useDebouncedCallback< maxWait, trailing, useRAF, - isClientSize, + isClientSide, debounceOnServer, ]); diff --git a/test/useDebounce.test.tsx b/test/useDebounce.test.tsx index 546b7a5..f211891 100644 --- a/test/useDebounce.test.tsx +++ b/test/useDebounce.test.tsx @@ -65,7 +65,7 @@ describe('useDebounce', () => { // timeout shouldn't have been called yet after leading call was executed // @ts-ignore - expect(screen.getByRole('test')).toHaveTextContent('Hello world'); + expect(screen.getByRole('test')).toHaveTextContent('Hello again'); act(() => { jest.runAllTimers(); @@ -283,13 +283,13 @@ describe('useDebounce', () => { const tree = render(); - expect(eq).toHaveBeenCalledTimes(1); + expect(eq).toHaveBeenCalledTimes(2); act(() => { tree.rerender(); }); - expect(eq).toHaveBeenCalledTimes(2); + expect(eq).toHaveBeenCalledTimes(4); expect(eq).toHaveBeenCalledWith('Hello', 'Test'); // Since the equality function always returns true, expect the value to stay the same // @ts-ignore @@ -414,4 +414,91 @@ describe('useDebounce', () => { // @ts-ignore expect(screen.getByRole('test')).toHaveTextContent('Hello world'); }); + + + it('Handles isPending', () => { + function Component({propValue}) { + const [value, fns] = useDebounce(propValue, 1000); + return ( +
+
{value}
+
{fns.isPending().toString()}
+
+ ); + } + + const tree = render(); + + // check inited value + // @ts-ignore + expect(screen.getByRole('value')).toHaveTextContent('Hello'); + // @ts-ignore + expect(screen.getByRole('pending')).toHaveTextContent('false'); + + act(() => { + tree.rerender(); + }); + // timeout shouldn't have called yet + // @ts-ignore + expect(screen.getByRole('value')).toHaveTextContent('Hello'); + // @ts-ignore + expect(screen.getByRole('pending')).toHaveTextContent('true'); + + act(() => { + jest.runAllTimers(); + }); + // after runAllTimer text should be updated + // @ts-ignore + expect(screen.getByRole('value')).toHaveTextContent('Hello 1'); + // @ts-ignore + expect(screen.getByRole('pending')).toHaveTextContent('false'); + }) + + it('Should handle isPending state correctly while switching between bounced values', () => { + function Component({propValue}) { + const [value, fns] = useDebounce(propValue, 1000); + return ( +
+
{value}
+
{fns.isPending().toString()}
+
+ ); + } + + const tree = render(); + + // check inited value + // @ts-ignore + expect(screen.getByRole('value')).toHaveTextContent('Hello'); + // @ts-ignore + expect(screen.getByRole('pending')).toHaveTextContent('false'); + + act(() => { + tree.rerender(); + }); + // timeout shouldn't have called yet + // @ts-ignore + expect(screen.getByRole('value')).toHaveTextContent('Hello'); + // @ts-ignore + expect(screen.getByRole('pending')).toHaveTextContent('true'); + + act(() => { + tree.rerender(); + }); + + // timeout shouldn't have called yet + // @ts-ignore + expect(screen.getByRole('value')).toHaveTextContent('Hello'); + // @ts-ignore + expect(screen.getByRole('pending')).toHaveTextContent('false'); + + act(() => { + jest.runAllTimers(); + }); + // after runAllTimer text should be updated + // @ts-ignore + expect(screen.getByRole('value')).toHaveTextContent('Hello'); + // @ts-ignore + expect(screen.getByRole('pending')).toHaveTextContent('false'); + }) });