Reputation: 803
I'm trying to add debouncing with lodash to a search function, called from an input onChange event. The code below generates a type error 'function is expected', which I understand because lodash is expecting a function. What is the right way to do this and can it be done all inline? I have tried nearly every example thus far on SO to no avail.
search(e){
let str = e.target.value;
debounce(this.props.relay.setVariables({ query: str }), 500);
},
Upvotes: 58
Views: 175229
Reputation: 2102
If your react architecture is complex, it can be hard to avoid capturing state using lodash's debounce function. It can ultimately be simpler to do a pure react version of debounce. The code below generates a new function every time the search state is changed, so the state captured is current, but manages the debounce via storing the call in a timer and cancelling via that ref.
const [lastSearch, setLastSearch] = useState('');
const [search, setSearch] = useState('');
const searchTimerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// avoid firing this effect initially
if (lastSearch === search) {
return;
}
setLastSearch(search);
// this cancels the previous call if it exists
if (searchTimerRef.current) {
clearTimeout(searchTimerRef.current);
}
searchTimerRef.current = setTimeout(() => {
callApi();
}, 750);
}, [search]);
const handleSearchChange = (search: string) => {
setSearch(search);
};
Upvotes: 1
Reputation: 1029
With a functional react component try using useCallback
. useCallback
memoizes your debounce function so it doesn't get recreated again and again when the component rerenders. Without useCallback
the debounce function will not sync with the next key stroke.
import {useCallback} from 'react';
import _debounce from 'lodash/debounce';
import axios from 'axios';
function Input() {
const [value, setValue] = useState('');
function handleDebounceFn(inputValue) {
axios.post('/endpoint', {
value: inputValue,
}).then((res) => {
console.log(res.data);
});
}
const debounceFn = useCallback(_debounce(handleDebounceFn, 1000), []);
function handleChange (event) {
setValue(event.target.value);
debounceFn(event.target.value);
};
return <input value={value} onChange={handleChange} />
}
Upvotes: 92
Reputation: 206
debounce utility function
import { useEffect, useRef, useState } from 'react';
export function useDebounce(value, debounceTimeout) {
const mounted = useRef(false);
const [state, setState] = useState(value);
useEffect(() => {
if (!mounted.current) {
mounted.current = true;
return;
}
const handler = setTimeout(() => setState(value), debounceTimeout);
return () => clearTimeout(handler);
}, [value]);
return state;
}
use it like this
const [titleFilterState, setTitleFilterState] = React?.useState('');
const debounceSearchValue = useDebounce(titleFilterState, 300);
then you can use debounceSearchValue as your title value its just it is going to be delayed to not make data fetch after each key press.
Upvotes: 0
Reputation: 586
if you have
React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead. eslintreact-hooks/exhaustive-deps
warning just use the useMemo
instead
const debounceFunc = useMemo(() => _debounce(func, 500), []);
Upvotes: 2
Reputation: 348
Improving on this answer: https://stackoverflow.com/a/67941248/2390312
Using useCallback
and debounce
is known to cause an eslint exhaustive deps
warning.
Here's how to do it with functional components and useMemo
import { useMemo } from 'react';
import { debounce } from 'lodash';
import axios from 'axios';
function Input() {
const [value, setValue] = useState('');
const debounceFn = useMemo(() => debounce(handleDebounceFn, 1000), []);
function handleDebounceFn(inputValue) {
axios.post('/endpoint', {
value: inputValue,
}).then((res) => {
console.log(res.data);
});
}
function handleChange (event) {
setValue(event.target.value);
debounceFn(event.target.value);
};
return <input value={value} onChange={handleChange} />
}
We are using useMemo
to return a memoized value, where this value is the function returned by debounce
Upvotes: 6
Reputation: 321
Some answers are neglecting that if you want to use something like e.target.value from the event object (e), the original event values will be null when you pass it through your debounce function.
See this error message:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property nativeEvent
on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().
As the message says, you have to include e.persist() in your event function. For example:
const onScroll={(e) => {
debounceFn(e);
e.persist();
}}
Then of course, your debounceFn needs to be scoped outside of the return statement in order to utilize React.useCallback(), which is necessary. My debounceFn looks like this:
const debounceFn = React.useCallback(
_.debounce((e) =>
calculatePagination(e),
500, {
trailing: true,
}
),
[]
);
Upvotes: 1
Reputation: 241
This is the correct FC approach @
Aximili answers triggers only one time
import { SyntheticEvent } from "react"
export type WithOnChange<T = string> = {
onChange: (value: T) => void
}
export type WithValue<T = string> = {
value: T
}
// WithValue & WithOnChange
export type VandC<A = string> = WithValue<A> & WithOnChange<A>
export const inputValue = (e: SyntheticEvent<HTMLElement & { value: string }>): string => (e.target as HTMLElement & { value: string }).value
const MyComponent: FC<VandC<string>> = ({ onChange, value }) => {
const [reload, setReload] = useState(false)
const [state, setstate] = useState(value)
useEffect(() => {
if (reload) {
console.log('called api ')
onChange(state)
setReload(false)
}
}, [reload])
const callApi = () => {
setReload(true)
} // You might be able to call API directly here, I haven't tried
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000))
function handleChange(x:string) {
setstate(x)
debouncedCallApi()
}
return (<>
<input
value={state} onChange={_.flow(inputValue, handleChange)} />
</>)
}
Upvotes: 0
Reputation: 644
const delayedHandleChange = debounce(eventData => someApiFunction(eventData), 500);
const handleChange = (e) => {
let eventData = { id: e.id, target: e.target };
delayedHandleChange(eventData);
}
Upvotes: -2
Reputation: 993
A lot of the answers here I found to be overly complicated or just inaccurate (i.e. not actually debouncing). Here's a straightforward solution with a check:
const [count, setCount] = useState(0); // simple check debounce is working
const handleChangeWithDebounce = _.debounce(async (e) => {
if (e.target.value && e.target.value.length > 4) {
// TODO: make API call here
setCount(count + 1);
console.log('the current count:', count)
}
}, 1000);
<input onChange={handleChangeWithDebounce}></input>
Upvotes: 3
Reputation: 9
This worked for me:
handleChange(event) {
event.persist();
const handleChangeDebounce = _.debounce((e) => {
if (e.target.value) {
// do something
}
}, 1000);
handleChangeDebounce(event);
}
Upvotes: 0
Reputation: 762
class MyComp extends Component {
debounceSave;
constructor(props) {
super(props);
}
this.debounceSave = debounce(this.save.bind(this), 2000, { leading: false, trailing: true });
}
save() is the function to be called
debounceSave() is the function you actually call (multiple times).
Upvotes: 0
Reputation: 513
for your case, it should be:
search = _.debounce((e){
let str = e.target.value;
this.props.relay.setVariables({ query: str });
}, 500),
Upvotes: 0
Reputation: 11
@Aximili
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));
looks strange :) I prefare solutions with useCallback
:
const [searchFor, setSearchFor] = useState('');
const changeSearchFor = debounce(setSearchFor, 1000);
const handleChange = useCallback(changeSearchFor, []);
Upvotes: 0
Reputation: 29464
This is how I had to do it after googling the whole day.
const MyComponent = (props) => {
const [reload, setReload] = useState(false);
useEffect(() => {
if(reload) { /* Call API here */ }
}, [reload]);
const callApi = () => { setReload(true) }; // You might be able to call API directly here, I haven't tried
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));
function handleChange() {
debouncedCallApi();
}
return (<>
<input onChange={handleChange} />
</>);
}
Upvotes: 22
Reputation: 5479
The debounce function can be passed inline in the JSX or set directly as a class method as seen here:
search: _.debounce(function(e) {
console.log('Debounced Event:', e);
}, 1000)
Fiddle: https://jsfiddle.net/woodenconsulting/69z2wepo/36453/
If you're using es2015+ you can define your debounce method directly, in your constructor
or in a lifecycle method like componentWillMount
.
Examples:
class DebounceSamples extends React.Component {
constructor(props) {
super(props);
// Method defined in constructor, alternatively could be in another lifecycle method
// like componentWillMount
this.search = _.debounce(e => {
console.log('Debounced Event:', e);
}, 1000);
}
// Define the method directly in your class
search = _.debounce((e) => {
console.log('Debounced Event:', e);
}, 1000)
}
Upvotes: 43
Reputation: 17579
That's not so easy question
On one hand to just work around error you are getting, you need to wrap up you setVariables
in the function:
search(e){
let str = e.target.value;
_.debounce(() => this.props.relay.setVariables({ query: str }), 500);
}
On another hand, I belive debouncing logic has to be incapsulated inside Relay.
Upvotes: 3