27
Testing useDebouncedValue hooks
A hook that debounces value to eliminate the performance penalty caused by rapid changes to a value. Source: usehooks.com
Should only emit the last value change when specified debounce time is past.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
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.
27