Perform debounce in React.js
Solution 1
2019: try hooks + promise debouncing
This is the most up to date version of how I would solve this problem. I would use:
- awesome-debounce-promise to debounce the async function
- use-constant to store that debounced function into the component
- react-async-hook to get the result into my component
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.
2018: try promise debouncing
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:
- API calls will be debounced
- the debounced function always returns a promise
- only the last call's returned promise will resolve
- a single
this.setState({ result });
will happen per API call
Eventually, 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.
< 2017: still want to use callback debouncing?
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.
GOOD IDEA:
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).
NOT a good idea:
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).
NOT a good idea:
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.
NOT a good idea:
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.
Take care of React's event pooling
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.
Without persist (default behavior: pooled event)
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.
With persist
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.
Solution 2
Uncontrolled Components
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
Controlled Components
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.
Solution 3
2019: Use the 'useCallback' react hook
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!
Solution 4
After struggling with the text inputs for a while and not finding a perfect solution on my own, I found this on npm: react-debounce-input.
Here is a simple example:
import React from 'react';
import ReactDOM from 'react-dom';
import {DebounceInput} from 'react-debounce-input';
class App extends React.Component {
state = {
value: ''
};
render() {
return (
<div>
<DebounceInput
minLength={2}
debounceTimeout={300}
onChange={event => this.setState({value: event.target.value})} />
<p>Value: {this.state.value}</p>
</div>
);
}
}
const appRoot = document.createElement('div');
document.body.appendChild(appRoot);
ReactDOM.render(<App />, appRoot);
The DebounceInput component accepts all of the props you can assign to a normal input element. Try it out on codepen
I hope it helps someone else too and saves them some time.
Solution 5
I found this post by Justin Tulk very helpful. After a couple of attempts, in what one would perceive to be the more official way with react/redux, it shows that it fails due to React's synthetic event pooling. His solution then uses some internal state to track the value changed/entered in the input, with a callback right after setState
which calls a throttled/debounced redux action that shows some results in realtime.
import React, {Component} from 'react'
import TextField from 'material-ui/TextField'
import { debounce } from 'lodash'
class TableSearch extends Component {
constructor(props){
super(props)
this.state = {
value: props.value
}
this.changeSearch = debounce(this.props.changeSearch, 250)
}
handleChange = (e) => {
const val = e.target.value
this.setState({ value: val }, () => {
this.changeSearch(val)
})
}
render() {
return (
<TextField
className = {styles.field}
onChange = {this.handleChange}
value = {this.props.value}
/>
)
}
}
Chetan Ankola
Updated on July 08, 2022Comments
-
Chetan Ankola almost 2 years
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 } });
-
Etai almost 10 yearsThis does not work for inputs. The event target in the debounced function no longer has a value... so the input stays empty.
-
Etai almost 10 yearsnevermind, it works in a jsfiddle, just not in my env for some reason... sorry. I edited your answer to add the jsfiddle, but I can't upvote until you edit your answer..
-
Eliseu Monar dos Santos almost 10 years@Etai If you are looking for a more complex example, check out (using RxJS): github.com/eliseumds/react-autocomplete
-
Alastair Maw almost 10 yearsSlightly complex, this. You have to be a bit careful about props. If you set
<input value={this.props.someprop}...
then it won't render properly as the update on keypress doesn't make it back into the component until after the debounce. It's fine to omit thevalue=
if you're happy for this to be unmanaged, but if you'd like to pre-populate the value and/or bind it somewhere else then obviously this doesn't work. -
julen almost 10 years@AlastairMaw the question had an uncontrolled component, that's why the reply has it too. I've added below an alternative version for controlled components, with a pre-populated value.
-
robbles over 9 yearsWon't this make the state/timing of the debounce global across all instances of InputField, because it's created with the class definition? Maybe that's what you want, but it's worth noting regardless.
-
Sebastien Lorber over 9 yearsthis is very dangerous if you mount the component mutiple times in the DOM, see stackoverflow.com/questions/23123138/…
-
Sebastien Lorber over 9 yearsdangerous if mounted multiple time in the dom, check stackoverflow.com/questions/23123138/…
-
julen over 9 yearsThanks @SebastienLorber for raising that, I've updated the answer and JSFiddles accordingly.
-
Aditya Sinha about 9 years
onChange : Event -> unit
takes an event, so you can rid yourself ofthis.refs.searchBox.getDOMNode().value
and replace it withevt.target.value
for more ideomatic javascript code. -
Aditya Sinha about 9 yearsBecause the event object is not immutable and is destroyed by ReactJS, so even if you wrap and attain a closure capture, the code will fail.
-
Aditya Sinha about 9 yearsThat's not debounce, it's 'delay'. Debounce resets the timeout every event that happens before the timeout. -1
-
Aditya Sinha about 9 yearsThis is a bad solution, because of double-mount issues -- you're making your function to scheduleChange a singleton and that's not a good idea. -1
-
canvaskisa about 9 years@Henrik My bad, you are right. By the way, it's easy to make debounce like this.
-
arush_try.com over 8 yearsSuperb answer, this is great for setting form fields state as 'interacting' for a few seconds after they stop typing, and then being able to cancel on form submit or onBlur
-
thom_nic over 8 yearsNote that in ES6, instead of defining your method inside the constructor (feels weird) you can do
handleOnChange = debounce((e) => { /* onChange handler code here */ }, timeout)
at the top level of your class. You're still effectively setting an instance member but it looks a bit more like a normal method definition. No need for aconstructor
if you don't already have one defined. I suppose it's mostly a style preference. -
elado over 8 yearsDon't forget to cancel the debounced method in
componentWillUnmount
:this.method.cancel()
- otherwise it might want to setState on an unmounted component. -
Jonas Kello about 8 yearsPerhaps add information about stateless components (which are just functions). I guess in that case things are different?
-
Sebastien Lorber about 8 years@JonasKello you can't debounce inside a stateless component because the debounced function is actually stateful. You need a stateful component to hold that debounced function, but you can call a stateless component with an already debounced function if needed.
-
chifliiiii almost 8 yearsWhy all answer includes _.debounce instead of writing the function ? It needs the whole library for that function ?
-
Sebastien Lorber almost 8 years@chifliiiii you don't need underscore to use this answer. I use underscore/lodash _ just because people are used to these libs for debouncing/throttling, but you can provide your own debouncing function and the answer stays the same. I'm removing the _ for clarity
-
Pavel Polyakov about 7 years@julen thanks for your examples. They still work. Could you, please, explain why having debounce in the separate method is so critical? In this example: jsfiddle.net/TzLZq/11 , the debounced method is called N times, instead of 1.
-
julen about 7 yearshi @PavelPolyakov, because the debounced function is asynchronous and the synthetic events will be nullified by the time it's executed. That's why
e.persist()
needs to go in a non-debounced event handler. Your example should look like jsfiddle.net/oza88dye. To learn more, you can check the docs on React's event pooling -
Pavel Polyakov about 7 yearshi @julen, thanks for the answer. In my case the issue was that _.debounce was created more the 1 time, not once per script. That was the case. Thanks for your examples anyhow!
-
Oliver Watkins almost 7 yearsHere is a jsfiddle using a standard javascript debounce method (not the underscore version) - jsfiddle.net/c7a0joha
-
tatsu over 6 yearsI would highly recommend editing this answer and showing imports contrary to what you say it is crucial and I'm not able to implement your answer in my code as is.
-
Xeoncross over 6 yearsI would recommend adding
{...this.props}
to theSearchBox.render(<input {...this.props}>
so you can do things like<SearchBox className="foo" ...
-
Igor Ilić over 6 yearsIt
this.method = debounce(this.method,1000);
should not work. You should bind this method to this (sic!):this.method = debounce(this.method.bind(this),1000);
-
Sebastien Lorber over 6 years@IgorIlić if you are using ES5 createClass syntax the method is already bound to this. Maybe you tried to mix 2 of the proposed solutions in an incorrect way? please share a jsfiddle if you still have this error.
-
Jonny Asmar over 6 yearsExcellent note on React's event pooling! Is there any concern with
e.persist()
of memory leakage? Do you need to handle GC here manually? If so, what is a good way to do that? -
Sebastien Lorber over 6 years@JonnyAsmar you don't need to do anything, the object is removed from the pool and it will just be garbage collected when it's not referenced anymore like any object, instead of being reused. As far as I know there's no imperative way to tell React to put back the event object in the pool and it's managed automatically for you.
-
Igor Ilić over 6 years@SebastienLorber Sorry, I am using es6 class syntax.
-
MrE over 6 yearswhile this is a great answer, I don't recommend using
persist
especially when there may be lots of events, like onmousemove
. I have seen code become totally unresponsive that way. It is much more efficient to extract the needed data from the native event in the event call, and then call the debounced / throttled function with the data only, NOT the event itself. No need to persist the event that way -
Breaker222 over 5 yearsthis solution does not answer the question, as it would trigger the action exactly after the specified timeout. But in this topic the timeout should be "extendable" if the debounce is called multiple times within the timeout.
-
Fadi Abo Msalam over 5 years@SebastienLorber can this lib be used with react-native ?
-
Tazo leladze over 5 yearsdefaultValue is what i want! Thank you very mach :)
-
Ian Kemp over 5 yearsThis answer is hideously out of date.
-
Hayi over 5 yearsi think this middleware must be the first one to be executed in
applyMiddleware(...)
chain if we have many -
Mote Zart about 5 yearsThis doesn't work for me. The state does not update. If I remove
_debounce
wrapper it works. I love this idea though! -
chad steele about 5 yearsI'd have to see your code to offer much here, but I suspect there's something else going on... hopefully this much more thorough answer will shed some light. stackoverflow.com/questions/23123138/…
-
J. Hesters about 5 yearsI think there is a small mistake in the hooks example. It should be
const [inputText, searchStarwarsHero] = useState('');
-
Sebastien Lorber about 5 years@J.Hesters reviewed it and for me there is nothing wrong. If you still think I'm wrong can you provide a gist with more detailed explainations?
-
J. Hesters about 5 years@SebastienLorber you are correct, I was tired yesterday, sorry. Great answer!
-
dave4jr about 5 years@SebastienLorber Thanks for this awesome answer. Although under the ES6 (Constructor) section where you have this.method = debounce(this.method,1000).. and then show method() below it. If you aren't binding this here, wouldn't you need to use an arrow function to define method() below? Someone commented on it above and your repsonse was if you were using ES5 createClass, but this is ES6 class syntax, a little confused by that. Thanks man!
-
Sebastien Lorber about 5 years@dave4jr yeah you are probably write i should bind to this before deboucing (only if this is used inside method i guess, but it's often the case). Great to know my answer was helpful
-
Jason Rice almost 5 yearsThe timeout isn't initialized and that first clearTimeout will be dealing with undefined for a param. Not good.
-
ducdhm over 4 years@SebastienLorber As I checked, seem like
searchStarwarsHero
is undefined in your code. Can you double-check it again? -
chovy over 4 years
event.target.value
is always empty for me when I useevent.persist()
-
Carl Edwards over 4 yearsExcellent solution if you're using hooks. You saved me many more hours of frustration. Thanks!
-
Vadorequest over 4 yearsAfter trying many solutions listed here, definitely was the easiest.
-
El Anonimo over 4 yearsCould you please explain on why the multiple calls happen in the first place? Does
debounce()
not consider theonChange()
callback to be the same callback method? -
El Anonimo over 4 yearsI modified this solution to get it to work in my app. First I had to move the line
const testFunc2 = useCallback(debounce((text) => console.log('testFunc2() has ran:', text), 1000) , []);
inside the body of the function component or React outputs an error message about hook use outside of it. Then in theonChange
event handler:<input type='text' name='name' className='th-input-container__input' onChange={evt => {testFunc2(evt.target.value);}}
. -
El Anonimo over 4 yearsHere is how I used this solution to let user type to an input then send a debounced API call with the input value once he's done typing. stackoverflow.com/questions/59358092/….
-
Rivas system almost 4 yearsAdding to the above answer ---- const someFunction = (text) => { dispatch({ type: "addText", payload: { id, text, }, }); }; <input type="text" defaultValue={text} onChange={(e) => handler(e.target.value)} />
-
Luis Febro over 3 yearsThis solution finally worked for me. Thanks!
-
dmitry.matora over 3 yearsThis indeed is SO much better solution! Not just because it uses least amount of code, it also allows debouncing class functions (unlike awesome-debounce-promise, which is nearly useless for that reason)
-
Markus Schulz over 3 yearslooks nice, if i understand right a use-case can look like the following snippet: const debounce = useDebounce(); const debouncedSearchInputHandler = (event) => { setSearchInput(event.target.value); debounce({fn: () => startRestCall(event.target.value), timeout: 1000}); };
-
andruso over 3 yearsStill the best choice in January 2021
-
amdev over 3 yearsPureeeeeeeee JS, Love it
-
Shoyeb Memon over 3 yearsnice solution for a state component.
-
keemahs over 3 yearsso if i want to fire an event everytime value is set, will i do it like this ? - useEffect(() => { // function here }, [value]);
-
user3006381 over 3 yearsWorked like a charm for me. Wrapped the bound handler function as above, then updated the state in the handler function based on the field input. Thanks!
-
Federico Budassi about 3 yearsThe simplest one, by far!
-
TheEhsanSarshar about 3 yearsI just copy-pasted your solution without even thinking about it. and it's working like a charm 😋
-
Tony Scialo about 3 yearslove this solution as it needs no new dependencies
-
Or Assayag about 3 yearsPrefect. Thanks.
-
Siva Kannan about 3 yearsPlease add comments for the downvoting, I have been using this code in my current app and its perfectly working
-
Brettins over 2 yearsIs this at all different from the original poster's solution?
-
Fiddle Freak over 2 yearsI warn people using
useCallback
with the debounce from lodash, this will reset all your page state to the initial loaded value. I don't recommend this. -
acfasj over 2 yearsabout 2019 example, what if
searchFunction
change? -
Sebastien Lorber over 2 years@acfasj you can add it to a ref and keep that ref up-to-date in a useLayoutEffect, it's annoying but it's a common pattern in react hooks
-
Zhivko Zhelev over 2 yearsYes, it's different here: debounce(handleChange , 200);
-
Muhammad Mubashirullah Durrani about 2 yearsThis only sends the last letter entered
-
xji about 2 yearsNot sure what the issue was that you had with
lodash
@FiddleFreak, though one can also use a custom version ofdebounce
function such as presented here: freecodecamp.org/news/javascript-debounce-example -
Kostanos about 2 yearshow do you cancel it if user component was unmounted?