Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perform debounce in React.js

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   } }); 
like image 297
Chetan Ankola Avatar asked Apr 17 '14 01:04

Chetan Ankola


People also ask

Why we use debounce in react?

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.

What is throttling and Debouncing in react?

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.


2 Answers

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.

like image 101
Sebastien Lorber Avatar answered Oct 06 '22 01:10

Sebastien Lorber


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.

like image 20
julen Avatar answered Oct 06 '22 01:10

julen