diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index b2f6de9711..e78250303a 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -4,6 +4,7 @@ export * from './useAsyncInterval'; export * from './useCallbackWithAction'; export { default as useContextOrThrow } from './useContextOrThrow'; export * from './useDebouncedCallback'; +export * from './useDelay'; export * from './useDependentState'; export * from './useEffectNTimesWhen'; export * from './useIsEqualMemo'; diff --git a/packages/react-hooks/src/useDelay.test.ts b/packages/react-hooks/src/useDelay.test.ts new file mode 100644 index 0000000000..8ab082c6f6 --- /dev/null +++ b/packages/react-hooks/src/useDelay.test.ts @@ -0,0 +1,55 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { useDelay } from './useDelay'; + +describe('useDelay', () => { + const delay500 = 500; + const delay1000 = 1000; + const delay2000 = 2000; + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should return true while delay is active and false when done', () => { + const { result } = renderHook(() => useDelay(delay1000)); + expect(result.current).toBe(true); + + act(() => jest.advanceTimersByTime(delay1000)); + + expect(result.current).toBe(false); + }); + + it('should cancel setter when unmounted', () => { + const { result, unmount } = renderHook(() => useDelay(delay1000)); + unmount(); + + act(() => jest.advanceTimersByTime(delay1000)); + + expect(result.current).toBe(true); + }); + + it('should reset when delayMs is changed', () => { + const { result, rerender } = renderHook( + ({ delayMs }) => useDelay(delayMs), + { + initialProps: { delayMs: delay1000 }, + } + ); + + act(() => jest.advanceTimersByTime(delay500)); + + rerender({ delayMs: delay2000 }); + + act(() => jest.advanceTimersByTime(delay1000)); + + expect(result.current).toBe(true); + + act(() => jest.advanceTimersByTime(delay1000)); + + expect(result.current).toBe(false); + }); +}); diff --git a/packages/react-hooks/src/useDelay.ts b/packages/react-hooks/src/useDelay.ts new file mode 100644 index 0000000000..d5689c957f --- /dev/null +++ b/packages/react-hooks/src/useDelay.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; + +/** + * Sets a delay, and returns a boolean indicating whether the delay is still active. + * @param delayMs The delay in milliseconds + * @returns A boolean indicating whether the delay is still active + */ +export function useDelay(delayMs: number): boolean { + const [isDelayed, setIsDelayed] = useState(true); + + useEffect(() => { + const timeout = setTimeout(() => { + setIsDelayed(false); + }, delayMs); + + return () => { + clearTimeout(timeout); + }; + }, [delayMs]); + + return isDelayed; +} + +export default useDelay;