DEV Community

Rex
Rex

Posted on • Edited on

5 2

Testing useDebouncedValue Hook

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:

  1. The test uses a React Component to test the sut's behaviour. It shows how the hook should be used.

  2. 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');
Enter fullscreen mode Exit fullscreen mode

If anyone knows why the above fails, please, please let me know, I would be most grateful.

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs