From 63ac6ec1844cb2f9ff76ffc4f6a5d9b613d74cc8 Mon Sep 17 00:00:00 2001 From: Sean Collings Date: Tue, 26 Nov 2024 18:53:00 -0700 Subject: [PATCH] Implemented useDebouncedState --- packages/hooks/src/hooks/useDebouncedState.ts | 13 ++++ packages/hooks/src/index.ts | 1 + .../hooks/tests/useDebouncedState.test.ts | 66 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 packages/hooks/src/hooks/useDebouncedState.ts create mode 100644 packages/hooks/tests/useDebouncedState.test.ts diff --git a/packages/hooks/src/hooks/useDebouncedState.ts b/packages/hooks/src/hooks/useDebouncedState.ts new file mode 100644 index 00000000..ee7828c1 --- /dev/null +++ b/packages/hooks/src/hooks/useDebouncedState.ts @@ -0,0 +1,13 @@ +import { useEffect, useState } from 'react'; + +export function useDebouncedState(value: T, delay: number): ReturnType> { + const [state, setState] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => setState(value), delay); + + return () => clearTimeout(handler); + }, [value, delay]); + + return [state, setState]; +} diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 48433897..61689efd 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -7,3 +7,4 @@ export { useRenderCount } from './hooks/lifecycle/useRenderCount'; export { useRender } from './hooks/lifecycle/useRender'; export { useUnmount } from './hooks/lifecycle/useUnmount'; export { useMount } from './hooks/lifecycle/useMount'; +export { useDebouncedState } from './hooks/useDebouncedState'; diff --git a/packages/hooks/tests/useDebouncedState.test.ts b/packages/hooks/tests/useDebouncedState.test.ts new file mode 100644 index 00000000..a3e828b0 --- /dev/null +++ b/packages/hooks/tests/useDebouncedState.test.ts @@ -0,0 +1,66 @@ +import { renderHook, act } from '@testing-library/react'; +import { useDebouncedState } from '../src'; +import { describe, it, expect, vi } from 'vitest'; + +describe('useDebouncedState', () => { + vi.useFakeTimers(); + + it('should initialize with the given value', () => { + const { result } = renderHook(() => useDebouncedState('initial', 500)); + expect(result.current[0]).toBe('initial'); + }); + + it('should update the state after the delay', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebouncedState(value, delay), { + initialProps: { value: 'initial', delay: 500 }, + }); + + rerender({ value: 'updated', delay: 500 }); + + act(() => { + vi.advanceTimersByTime(500); + }); + + expect(result.current[0]).toBe('updated'); + }); + + it('should not update the state before the delay', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebouncedState(value, delay), { + initialProps: { value: 'initial', delay: 500 }, + }); + + rerender({ value: 'updated', delay: 500 }); + + act(() => { + vi.advanceTimersByTime(300); + }); + + expect(result.current[0]).toBe('initial'); + }); + + it('should reset the timer if value changes before delay', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebouncedState(value, delay), { + initialProps: { value: 'initial', delay: 500 }, + }); + + rerender({ value: 'updated1', delay: 500 }); + + act(() => { + vi.advanceTimersByTime(300); + }); + + rerender({ value: 'updated2', delay: 500 }); + + act(() => { + vi.advanceTimersByTime(300); + }); + + expect(result.current[0]).toBe('initial'); + + act(() => { + vi.advanceTimersByTime(200); + }); + + expect(result.current[0]).toBe('updated2'); + }); +});