Testing useDebouncedValue hooks

The Subject Under Test(sut):
A hook that debounces value to eliminate the performance penalty caused by rapid changes to a value. Source: usehooks.com
Behaviour:
Should only emit the last value change when specified debounce time is past.
Code:
import { useDebouncedValue } from './useDebouncedValue';
import { act } from '@testing-library/react-hooks';
import { useState } from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function TestComponent({ initialValue = 0 }: { initialValue?: number }) {
const [value, setValue] = useState(initialValue);
const debouncedValue = useDebouncedValue(value, 1000);
return (
<div>
<button onClick={() => setValue(value + 1)}>Increment</button>
<span data-testid={'debouncedValue'}>{debouncedValue}</span>
<span data-testid={'value'}>{value}</span>
</div>
);
}
describe('useDebouncedValue', function () {
afterEach(() => {
jest.useRealTimers();
});
it('should debounce and only change value when delay time has passed', function () {
jest.useFakeTimers();
const { getByTestId, getByText } = render(<TestComponent />);
const incrementButton = getByText('Increment');
const debouncedValue = getByTestId('debouncedValue');
const value = getByTestId('value');
const incrementAndPassTime = (passedTime: number) => {
act(() => {
userEvent.click(incrementButton);
jest.advanceTimersByTime(passedTime);
});
};
incrementAndPassTime(100);
expect(debouncedValue.textContent).toBe('0');
expect(value.textContent).toBe('1');
incrementAndPassTime(500);
expect(debouncedValue.textContent).toBe('0');
expect(value.textContent).toBe('2');
incrementAndPassTime(999);
expect(debouncedValue.textContent).toBe('0');
expect(value.textContent).toBe('3');
act(() => {
jest.advanceTimersByTime(1);
});
expect(debouncedValue.textContent).toBe('3');
expect(value.textContent).toBe('3');
});
});
describe('Initial Value of DebouncedValue', function () {
it('should set initial value', function () {
const { getByTestId } = render(<TestComponent key={'1'} initialValue={1} />);
expect(getByTestId('debouncedValue').textContent).toBe('1');
expect(getByTestId('value').textContent).toBe('1');
});
});
import { useEffect, useState } from 'react';
// Hook
export function useDebouncedValue(value: any, delay = 250) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
Notes:
  • The test uses a React Component to test the sut's behaviour. It shows how the hook should be used.

  • The Test uses useFakeTimers to control the pass of time and assert when the debounced value should and should not be changed

  • One weird and interesting behavior of Jest's fake timer is that the fake timer somehow got influenced by other tests before it:
    If I move the second describe block (not using the fake timer) before the first, the last assessment fails:
    act(() => {
          jest.advanceTimersByTime(1);
        });
    
        expect(debouncedValue.textContent).toBe('3'); // fails, got 0 instead of 3
        expect(value.textContent).toBe('3');
    If anyone knows why the above fails, please, please let me know, I would be most grateful.

    30

    This website collects cookies to deliver better user experience

    Testing useDebouncedValue hooks