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 work.
function debounce(fn, delay) { var timer = null; return function() { var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function() { fn.apply(context, args); }, delay); }; } var SearchBox = React.createClass({ render: function() { return <input type="search" name="p" onChange={this.handleOnChange} />; }, handleOnChange: function(event) { // make ajax call } });
A Debouncing Events in ReactJS will allow you to call a function that ensures that a time-consuming task does not fire so often. It's a function that takes a function as a parameter and wraps that function in a closure and returns it so this new function displays the “wait for a bit” behavior.
If we decide to prevent the second process from happening by making sure that our function can only run once in a given interval, that would be throttling. For our city filter app, we'll be using debouncing to solve our problem.
This is the most up to date version of how I would solve this problem. I would use:
This is some initial wiring but you are composing primitive blocks on your own, and you can make your own custom hook so that you only need to do this once.
// Generic reusable hook const useDebouncedSearch = (searchFunction) => { // Handle the input text state const [inputText, setInputText] = useState(''); // Debounce the original search async function const debouncedSearchFunction = useConstant(() => AwesomeDebouncePromise(searchFunction, 300) ); // The async callback is run each time the text changes, // but as the search function is debounced, it does not // fire a new request on each keystroke const searchResults = useAsync( async () => { if (inputText.length === 0) { return []; } else { return debouncedSearchFunction(inputText); } }, [debouncedSearchFunction, inputText] ); // Return everything needed for the hook consumer return { inputText, setInputText, searchResults, }; };
And then you can use your hook:
const useSearchStarwarsHero = () => useDebouncedSearch(text => searchStarwarsHeroAsync(text)) const SearchStarwarsHeroExample = () => { const { inputText, setInputText, searchResults } = useSearchStarwarsHero(); return ( <div> <input value={inputText} onChange={e => setInputText(e.target.value)} /> <div> {searchResults.loading && <div>...</div>} {searchResults.error && <div>Error: {search.error.message}</div>} {searchResults.result && ( <div> <div>Results: {search.result.length}</div> <ul> {searchResults.result.map(hero => ( <li key={hero.name}>{hero.name}</li> ))} </ul> </div> )} </div> </div> ); };
You will find this example running here and you should read react-async-hook documentation for more details.
We often want to debounce API calls to avoid flooding the backend with useless requests.
In 2018, working with callbacks (Lodash/Underscore) feels bad and error-prone to me. It's easy to encounter boilerplate and concurrency issues due to API calls resolving in an arbitrary order.
I've created a little library with React in mind to solve your pains: awesome-debounce-promise.
This should not be more complicated than that:
const searchAPI = text => fetch('/search?text=' + encodeURIComponent(text)); const searchAPIDebounced = AwesomeDebouncePromise(searchAPI, 500); class SearchInputAndResults extends React.Component { state = { text: '', results: null, }; handleTextChange = async text => { this.setState({ text, results: null }); const result = await searchAPIDebounced(text); this.setState({ result }); }; }
The debounced function ensures that:
this.setState({ result });
will happen per API callEventually, you may add another trick if your component unmounts:
componentWillUnmount() { this.setState = () => {}; }
Note that Observables (RxJS) can also be a great fit for debouncing inputs, but it's a more powerful abstraction which may be harder to learn/use correctly.
The important part here is to create a single debounced (or throttled) function per component instance. You don't want to recreate the debounce (or throttle) function everytime, and you don't want either multiple instances to share the same debounced function.
I'm not defining a debouncing function in this answer as it's not really relevant, but this answer will work perfectly fine with _.debounce
of underscore or lodash, as well as any user-provided debouncing function.
Because debounced functions are stateful, we have to create one debounced function per component instance.
ES6 (class property): recommended
class SearchBox extends React.Component { method = debounce(() => { ... }); }
ES6 (class constructor)
class SearchBox extends React.Component { constructor(props) { super(props); this.method = debounce(this.method.bind(this),1000); } method() { ... } }
ES5
var SearchBox = React.createClass({ method: function() {...}, componentWillMount: function() { this.method = debounce(this.method.bind(this),100); }, });
See JsFiddle: 3 instances are producing 1 log entry per instance (that makes 3 globally).
var SearchBox = React.createClass({ method: function() {...}, debouncedMethod: debounce(this.method, 100); });
It won't work, because during class description object creation, this
is not the object created itself. this.method
does not return what you expect because the this
context is not the object itself (which actually does not really exist yet BTW as it is just being created).
var SearchBox = React.createClass({ method: function() {...}, debouncedMethod: function() { var debounced = debounce(this.method,100); debounced(); }, });
This time you are effectively creating a debounced function that calls your this.method
. The problem is that you are recreating it on every debouncedMethod
call, so the newly created debounce function does not know anything about former calls! You must reuse the same debounced function over time or the debouncing will not happen.
var SearchBox = React.createClass({ debouncedMethod: debounce(function () {...},100), });
This is a little bit tricky here.
All the mounted instances of the class will share the same debounced function, and most often this is not what you want!. See JsFiddle: 3 instances are producting only 1 log entry globally.
You have to create a debounced function for each component instance, and not a single debounced function at the class level, shared by each component instance.
This is related because we often want to debounce or throttle DOM events.
In React, the event objects (i.e., SyntheticEvent
) that you receive in callbacks are pooled (this is now documented). This means that after the event callback has be called, the SyntheticEvent you receive will be put back in the pool with empty attributes to reduce the GC pressure.
So if you access SyntheticEvent
properties asynchronously to the original callback (as may be the case if you throttle/debounce), the properties you access may be erased. If you want the event to never be put back in the pool, you can use the persist()
method.
onClick = e => { alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`); setTimeout(() => { alert(`async -> hasNativeEvent=${!!e.nativeEvent}`); }, 0); };
The 2nd (async) will print hasNativeEvent=false
because the event properties have been cleaned up.
onClick = e => { e.persist(); alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`); setTimeout(() => { alert(`async -> hasNativeEvent=${!!e.nativeEvent}`); }, 0); };
The 2nd (async) will print hasNativeEvent=true
because persist
allows you to avoid putting the event back in the pool.
You can test these 2 behaviors here: JsFiddle
Read Julen's answer for an example of using persist()
with a throttle/debounce function.
You can use the event.persist()
method.
An example follows using underscore's _.debounce()
:
var SearchBox = React.createClass({ componentWillMount: function () { this.delayedCallback = _.debounce(function (event) { // `event.target` is accessible now }, 1000); }, onChange: function (event) { event.persist(); this.delayedCallback(event); }, render: function () { return ( <input type="search" onChange={this.onChange} /> ); } });
Edit: See this JSFiddle
Update: the example above shows an uncontrolled component. I use controlled elements all the time so here's another example of the above, but without using the event.persist()
"trickery".
A JSFiddle is available as well. Example without underscore
var SearchBox = React.createClass({ getInitialState: function () { return { query: this.props.query }; }, componentWillMount: function () { this.handleSearchDebounced = _.debounce(function () { this.props.handleSearch.apply(this, [this.state.query]); }, 500); }, onChange: function (event) { this.setState({query: event.target.value}); this.handleSearchDebounced(); }, render: function () { return ( <input type="search" value={this.state.query} onChange={this.onChange} /> ); } }); var Search = React.createClass({ getInitialState: function () { return { result: this.props.query }; }, handleSearch: function (query) { this.setState({result: query}); }, render: function () { return ( <div id="search"> <SearchBox query={this.state.result} handleSearch={this.handleSearch} /> <p>You searched for: <strong>{this.state.result}</strong></p> </div> ); } }); React.render(<Search query="Initial query" />, document.body);
Edit: updated examples and JSFiddles to React 0.12
Edit: updated examples to address the issue raised by Sebastien Lorber
Edit: updated with jsfiddle that does not use underscore and uses plain javascript 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