I'm trying to add debounce functionality to my React app, and want to do it without a library like loadash, 3rd party node module, etc. I tried with some posts there, but nothing worked for me.
Basically, in handleSearch
I just dispatch redux action which performs a query to API endpoint and in input bind value to pice of redux state and call handleSearch onChange.
My code is:
const handleSearch = (e: React.FormEvent<HTMLInputElement>) => {
const value = e.currentTarget.value
dispatch(setSearch(value))
}
And later in return
<input type="text" value={searchQuery} onChange={handleSearch} />
Also, my action:
export const searchMovies = (category: String, searchQuery: string) => async (dispatch: Dispatch<ControlDispatchTypes>) => {
try {
dispatch({
type: SEARCH_LIST_LOADING
})
let res: any;
if (searchQuery.length >= 3) {
res = await axios.get(`https://api.themoviedb.org/3/search/${category}?api_key=xxxxxxxxxx&query=${searchQuery}`)
}
dispatch({
type: SEARCH_LIST_SUCCESS,
payload: res.data.results
})
} catch (error) {
dispatch({
type: SEARCH_LIST_FAIL
})
}
}
and a piece of the reducer used for the search:
...
case SET_SEARCH:
return {
...state,
search: action.payload
}
case SEARCH_LIST_LOADING:
return {
...state,
searchLoading: false
}
case SEARCH_LIST_SUCCESS:
return {
...state,
searchLoading: false,
items: action.payload
}
case SEARCH_LIST_FAIL:
return {
...state,
searchLoading: false
}
...
Here's a simple implementation : import React, { useCallback } from "react"; import { debounce } from "lodash"; const handler = useCallback(debounce(someFunction, 2000), []); const onChange = (event) => { // perform any event related action here handler(); };
Another way to implement debouncing is using lodash. Lodash provides a debounce method that we can use to limit the rate of execution of the handleChange function. Just wrap the callback function with the debounce method and provide the amount of delay we want between two events.
It isn't clear how you are calling searchMovies()
but I think the easiest way of doing this kind of thing is by wrapping your dispatch in a callback that's been debounced.
const debounce = (fn, delay) => {
let timeout = -1;
return (...args) => {
if (timeout !== -1) {
clearTimeout(timeout);
}
timeout = setTimeout(fn, delay, ...args);
};
};
export const App = () => {
const query = useSelector(selectQuery);
const dispatch = useDispatch();
const requestMovies = useMemo(() => {
return debounce((query) => {
dispatch(searchMovies(query))
}, 300);
}, []);
const onQueryChange = useCallback((q) => {
dispatch(setSearch(q));
requestMovies(q);
}, []);
return (
<div className="App">
<input value={query} onChange={(e) => onQueryChange(e.currentTarget.value)} />
</div>
);
}
That is the simple solution. If you want bonus points consider writing a custom hook to pull contain all of that state and logic. It cleans up the components and makes the movie search reusable.
const QUERY_DEBOUNCE_PERIOD = 400;
const useMovieSearch = () => {
const dispatch = useDispatch();
const query = useSelector(selectQuery);
const category= useSelector(selectCategory);
const fence = useRef("");
const requestMovies = useMemo(() => {
const request = async (category, query) => {
const uri = `https://api.themoviedb.org/3/search/${category}?api_key=xxxxxxxxxx&query=${query}`;
fence.current = uri;
dispatch({
type: SEARCH_LIST_LOADING
});
try {
if (query.length >= 3) {
const res = await axios.get(uri);
if (fence.current === uri) {
dispatch({
type: SEARCH_LIST_SUCCESS,
payload: res.data.results
});
}
}
} catch (error) {
dispatch({
type: SEARCH_LIST_FAIL
})
}
};
return debounce(request, QUERY_DEBOUNCE_PERIOD);
}, []);
const searchMovies = useCallback((category, query) => {
dispatch({ type: SET_SEARCH, payload: query });
requestMovies(category, query);
}, [requestMovies]);
return {
query,
category,
searchMovies
};
};
This should look like pretty standard stuff. I just moved all of the searchMovies
logic into a custom hook. I also added fencing. Because of the asynchrony of the internet, you are not guaranteed to get results in the order the request were sent out. This simply ignores all responses except for the most recent request.
Usage is pretty much what you'd expect.
const SearchBar = () => {
const [query, category, searchMovies] = useMovieSearch();
return <input value={query} onChange={(e) => searchMovies(category, e.currentTarget.value)} />;
};
I created a custom hook that encapsulates the debouncing.
import { useMemo, useState } from 'react';
const debounce = (fn: any, delay: number) => {
let timeout = -1;
return (...args: any[]) => {
if (timeout !== -1) {
clearTimeout(timeout);
}
timeout = setTimeout(fn, delay, ...args);
};
};
export function useStateDebounced<T>(initialValue: T, delay: number) {
const [inputValue, _setInputValue] = useState<T>(initialValue);
const [debouncedInputValue, setDebouncedInputValue] = useState<T>(
initialValue
);
const memoizedDebounce = useMemo(
() =>
debounce((value: T) => {
setDebouncedInputValue(value);
}, delay),
[delay]
);
const setInputValue = (value: T | ((prevState: T) => T)) => {
if (value instanceof Function) {
_setInputValue((p) => {
const mutated = value(p);
memoizedDebounce(mutated);
return mutated;
});
} else {
_setInputValue(value);
memoizedDebounce(value);
}
};
return [inputValue, debouncedInputValue, setInputValue] as const;
}
Usage in code:
export default function App() {
const [value, debouncedValue, setValue] = useStateDebounced('', 1000);
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
return (
<div className="App">
{debouncedValue}
<input type="text" value={value} onChange={handleInputChange} />
</div>
);
}
View on CodeSandbox
Edit:
Just use https://www.npmjs.com/package/use-debounce
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With