I faced a problem of choice between my useDebouncedValue hook and react’s useDerredValue in my particular case.
First let me introduce how my useDebouncedValue hook looks:
import { useEffect, useState } from 'react';
const useDebouncedValue = <T>(value: T, delayMs: number) => {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timeoutId = setTimeout(() => {
setDebouncedValue(value);
}, delayMs);
return () => {
clearTimeout(timeoutId);
};
}, [debouncedValue, delayMs, value]);
return debouncedValue;
};
export { useDebouncedValue };
In my search form component I use the useDebouncedValue hook for query input in the following fashion:
/* I omitted some lines of code for brewity */
const SearchForm = ({ customClassName }: SearchFormProps) => {
const [searchQuery, setSearchQuery] = useState('');
/* todo: maybe I should use new useDeferredValue react-hook here ? */
const debouncedSearchQuery = useDebouncedValue(searchQuery, 250);
return (
<form
name={`searchForm`}
autoComplete={'off'}
className={cn(customClassName, searchform)}
ref={ref}
onSubmit={onSubmit}
>
<input
type='text'
name={queryParameters.productSearchQuery}
placeholder={'I am looking for...'}
className={searchform__input}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button type={`submit`} className={cn(button, searchform__button)}>
<SearchIcon className={searchform__icon} />
</button>
<SearchResults
query={debouncedSearchQuery}
isFocused={isFocused}
setIsFocused={setIsVisible}
/>
</form>
);
};
export { SearchForm };
Then in SearchResults component (which is wrapped in memo) I fetch a list of matched products in infinite scroll way from back end, so the list of products can be quite long. Fetched products may have variations in the corresponding array property (rest of properties are ommitted for brewity):
interface ProductDTO extends IEntityId {
name: string;
variations: {
ids: Id[];
byId: {
[key: string]: {
name: string;
}
}
};
}
If product has variations I render all its variations that match search query by full name (name of product + variation’s name):
Object.values(product.variations.byId)
.filter((variation) => {
const fullName = product.name + ' ' + variation.name;
return fullName.toLowerCase().includes(query.toLowerCase());
}).map(/*...render search result card*/)
So I guess in the context of long list of products such filtering job can take some time.
Benefit of using useDebouncedValue in this context:
- app doesn’t make many unnecessary requests during input.
- as SearchResults component is wrapped by
memo
and debouncedValue doesn’t change during input, our searchQuery state in SearchForm updates without any lag, so we kind of get a proper UX.
BUT! The con comes under the next circumstances: when user stops typing -> debounced value is assigned the latest searchQuery value -> SearchResults component starts to fetch new portion of products, filters them, renders and at this point of time user tries to type something and BOOM! user’s input is lagging resulting in bad UX.
useDeferredValue seems to solve the BUT issue by interrupting the SearchResults component’s render. But in case of using useDeferredValue we resort to making lots of unnecessary requests. By the way I use react-query library’s useQuery hook, which allows me to easily cancel unfinished requests. So I incline to the decision of leveraging useDeferredValue + useQuery with unfinished requests cancel.
What do you think would be the best option for my case?