How do you perform debounce in React.js?
I want to debounce the handleOnChange.
I tried with debounce(this.handleOnChange, 200)
but it doesn\'t
This solution does not need any extra lib and it also fires things up when the user presses enter:
const debounce = (fn, delay) => {
let timer = null;
return function() {
const context = this,
args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}
const [search, setSearch] = useState('');
const [searchFor, setSearchFor] = useState(search);
useEffect(() => {
console.log("Search:", searchFor);
}, [searchFor]);
const fireChange = event => {
const { keyCode } = event;
if (keyCode === 13) {
event.preventDefault();
setSearchFor(search);
}
}
const changeSearch = event => {
const { value } = event.target;
setSearch(value);
debounceSetSearchFor(value);
};
const debounceSetSearchFor = useCallback(debounce(function(value) {
setSearchFor(value);
}, 250), []);
and the input could be like:
<input value={search} onKeyDown={fireChange} onChange={changeSearch} />
Here's a working TypeScript example for those who use TS and want to debounce async
functions.
function debounce<T extends (...args: any[]) => any>(time: number, func: T): (...funcArgs: Parameters<T>) => Promise<ReturnType<T>> {
let timeout: Timeout;
return (...args: Parameters<T>): Promise<ReturnType<T>> => new Promise((resolve) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
resolve(func(...args));
}, time)
});
}
Avoid using event.persist()
- you want to let React recycle the synthetic event. I think the cleanest way whether you use classes or hooks is to split the callback into two pieces:
Classes
handleMouseOver = throttle(target => {
console.log(target);
}, 1000);
onMouseOver = e => {
this.handleMouseOver(e.target);
};
<div onMouseOver={this.onMouseOver} />
Functions
const handleMouseOver = useRef(throttle(target => {
console.log(target);
}, 1000));
function onMouseOver(e) {
handleMouseOver.current(e.target);
}
<div onMouseOver={this.onMouseOver} />
Note that if your handleMouseOver
function uses state from within the component, you should use useMemo
instead of useRef
and pass those as dependencies otherwise you will be working with stale data (does not apply to classes of course).
Here is an example I came up with that wraps another class with a debouncer. This lends itself nicely to being made into a decorator/higher order function:
export class DebouncedThingy extends React.Component {
static ToDebounce = ['someProp', 'someProp2'];
constructor(props) {
super(props);
this.state = {};
}
// On prop maybe changed
componentWillReceiveProps = (nextProps) => {
this.debouncedSetState();
};
// Before initial render
componentWillMount = () => {
// Set state then debounce it from here on out (consider using _.throttle)
this.debouncedSetState();
this.debouncedSetState = _.debounce(this.debouncedSetState, 300);
};
debouncedSetState = () => {
this.setState(_.pick(this.props, DebouncedThingy.ToDebounce));
};
render() {
const restOfProps = _.omit(this.props, DebouncedThingy.ToDebounce);
return <Thingy {...restOfProps} {...this.state} />
}
}
After trying many different approaches, I found using useCallback
to be the simplest and most efficient at solving the multiple calls problem of using debounce
within an onChange
event.
As per the Hooks API documentation,
useCallback returns a memorized version of the callback that only changes if one of the dependencies has changed.
Passing an empty array as a dependency makes sure the callback is called only once. 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();
};
Hope this helps!
There is now another solution for React and React Native in late/2019:
react-debounce-component
<input>
<Debounce ms={500}>
<List/>
</Debounce>
It's a component, easy to use, tiny and widley supported
import React from 'react';
import Debounce from 'react-debounce-component';
class App extends React.Component {
constructor (props) {
super(props);
this.state = {value: 'Hello'}
}
render () {
return (
<div>
<input value={this.state.value} onChange={(event) => {this.setState({value: event.target.value})}}/>
<Debounce ms={1000}>
<div>{this.state.value}</div>
</Debounce>
</div>
);
}
}
export default App;
*I'm the creator of this component