How to Test useDeferredValue Hook in React
Testing React’s useDeferredValue hook requires an
understanding of concurrent rendering and how React schedules
low-priority UI updates. This article provides a direct, step-by-step
guide on how to write unit tests for components utilizing
useDeferredValue using React Testing Library and Jest,
focusing on verifying both the immediate and deferred states of your
UI.
Understanding the Testing Challenge
The useDeferredValue hook accepts a value and returns a
new copy of that value that will defer to more urgent updates. During an
urgent update (like typing in an input), the deferred value remains at
its previous state. Once the urgent render completes, React schedules a
low-priority render in the background with the new deferred value.
To test this behavior, your tests must assert two distinct phases: 1. The urgent phase: The input value updates immediately, but the deferred value remains unchanged. 2. The deferred phase: The deferred value eventually catches up and renders the new state.
The Component under Test
Consider this simple search component that uses
useDeferredValue to delay a heavy list filtering
process:
import { useState, useDeferredValue } from 'react';
export function SearchComponent() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<div>
<input
type="text"
placeholder="Search..."
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p data-testid="immediate-value">Immediate: {text}</p>
<p data-testid="deferred-value">Deferred: {deferredText}</p>
</div>
);
}Writing the Test with React Testing Library
To test this component, you need @testing-library/react
and @testing-library/user-event. Because
useDeferredValue relies on React’s concurrent scheduler,
you must use asynchronous utilities like waitFor or
findBy to catch the deferred update.
Here is how to write the test case:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SearchComponent } from './SearchComponent';
test('defers updating the deferred value during rapid input', async () => {
const user = userEvent.setup();
render(<SearchComponent />);
const input = screen.getByPlaceholderText('Search...');
const immediateText = screen.getByTestId('immediate-value');
const deferredText = screen.getByTestId('deferred-value');
// 1. Initial State
expect(immediateText).toHaveTextContent('Immediate:');
expect(deferredText).toHaveTextContent('Deferred:');
// 2. Trigger an update
await user.type(input, 'React');
// 3. Verify immediate state is updated
expect(immediateText).toHaveTextContent('Immediate: React');
// 4. Verify deferred state eventually matches
await waitFor(() => {
expect(deferredText).toHaveTextContent('Deferred: React');
});
});Key Tactics for Testing Concurrent Hooks
- Use
userEventoverfireEvent: TheuserEventlibrary simulates real browser interactions more accurately, which is crucial for triggering React’s concurrent scheduling mechanisms properly. - Leverage
waitFor: Because React schedules the deferred render at a lower priority, assertions on the deferred output should always be wrapped inwaitForor queried usingfindBymethods. This ensures the test runner waits for React’s background task queue to clear. - Avoid wrapping in manual
actcalls: Modern versions of React Testing Library handleactboundaries automatically under the hood when usinguserEventandwaitFor, keeping your test code clean and readable.