Perform debounce in React.js

415,472

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:

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}
        />
    )
  }
}
Share:
415,472
Chetan Ankola
Author by

Chetan Ankola

Updated on July 08, 2022

Comments

  • Chetan Ankola
    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
    Etai almost 10 years
    This does not work for inputs. The event target in the debounced function no longer has a value... so the input stays empty.
  • Etai
    Etai almost 10 years
    nevermind, 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
    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
    Alastair Maw almost 10 years
    Slightly 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 the value= 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
    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
    robbles over 9 years
    Won'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
    Sebastien Lorber over 9 years
    this is very dangerous if you mount the component mutiple times in the DOM, see stackoverflow.com/questions/23123138/…
  • Sebastien Lorber
    Sebastien Lorber over 9 years
    dangerous if mounted multiple time in the dom, check stackoverflow.com/questions/23123138/…
  • julen
    julen over 9 years
    Thanks @SebastienLorber for raising that, I've updated the answer and JSFiddles accordingly.
  • Aditya Sinha
    Aditya Sinha about 9 years
    onChange : Event -> unit takes an event, so you can rid yourself of this.refs.searchBox.getDOMNode().value and replace it with evt.target.value for more ideomatic javascript code.
  • Aditya Sinha
    Aditya Sinha about 9 years
    Because 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
    Aditya Sinha about 9 years
    That's not debounce, it's 'delay'. Debounce resets the timeout every event that happens before the timeout. -1
  • Aditya Sinha
    Aditya Sinha about 9 years
    This 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
    canvaskisa about 9 years
    @Henrik My bad, you are right. By the way, it's easy to make debounce like this.
  • arush_try.com
    arush_try.com over 8 years
    Superb 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
    thom_nic over 8 years
    Note 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 a constructor if you don't already have one defined. I suppose it's mostly a style preference.
  • elado
    elado over 8 years
    Don't forget to cancel the debounced method in componentWillUnmount: this.method.cancel() - otherwise it might want to setState on an unmounted component.
  • Jonas Kello
    Jonas Kello about 8 years
    Perhaps add information about stateless components (which are just functions). I guess in that case things are different?
  • Sebastien Lorber
    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
    chifliiiii almost 8 years
    Why all answer includes _.debounce instead of writing the function ? It needs the whole library for that function ?
  • Sebastien Lorber
    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
    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
    julen about 7 years
    hi @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
    Pavel Polyakov about 7 years
    hi @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
    Oliver Watkins almost 7 years
    Here is a jsfiddle using a standard javascript debounce method (not the underscore version) - jsfiddle.net/c7a0joha
  • tatsu
    tatsu over 6 years
    I 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
    Xeoncross over 6 years
    I would recommend adding {...this.props} to the SearchBox.render(<input {...this.props}> so you can do things like <SearchBox className="foo" ...
  • Igor Ilić
    Igor Ilić over 6 years
    It 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
    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
    Jonny Asmar over 6 years
    Excellent 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
    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ć
    Igor Ilić over 6 years
    @SebastienLorber Sorry, I am using es6 class syntax.
  • MrE
    MrE over 6 years
    while this is a great answer, I don't recommend using persist especially when there may be lots of events, like on mousemove. 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
    Breaker222 over 5 years
    this 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
    Fadi Abo Msalam over 5 years
    @SebastienLorber can this lib be used with react-native ?
  • Tazo leladze
    Tazo leladze over 5 years
    defaultValue is what i want! Thank you very mach :)
  • Ian Kemp
    Ian Kemp over 5 years
    This answer is hideously out of date.
  • Hayi
    Hayi over 5 years
    i think this middleware must be the first one to be executed in applyMiddleware(...) chain if we have many
  • Mote Zart
    Mote Zart about 5 years
    This doesn't work for me. The state does not update. If I remove _debounce wrapper it works. I love this idea though!
  • chad steele
    chad steele about 5 years
    I'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
    J. Hesters about 5 years
    I think there is a small mistake in the hooks example. It should be const [inputText, searchStarwarsHero] = useState('');
  • Sebastien Lorber
    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
    J. Hesters about 5 years
    @SebastienLorber you are correct, I was tired yesterday, sorry. Great answer!
  • dave4jr
    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
    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
    Jason Rice almost 5 years
    The timeout isn't initialized and that first clearTimeout will be dealing with undefined for a param. Not good.
  • ducdhm
    ducdhm over 4 years
    @SebastienLorber As I checked, seem like searchStarwarsHero is undefined in your code. Can you double-check it again?
  • chovy
    chovy over 4 years
    event.target.value is always empty for me when I use event.persist()
  • Carl Edwards
    Carl Edwards over 4 years
    Excellent solution if you're using hooks. You saved me many more hours of frustration. Thanks!
  • Vadorequest
    Vadorequest over 4 years
    After trying many solutions listed here, definitely was the easiest.
  • El Anonimo
    El Anonimo over 4 years
    Could you please explain on why the multiple calls happen in the first place? Does debounce() not consider the onChange() callback to be the same callback method?
  • El Anonimo
    El Anonimo over 4 years
    I 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 the onChange event handler: <input type='text' name='name' className='th-input-container__input' onChange={evt => {testFunc2(evt.target.value);}}.
  • El Anonimo
    El Anonimo over 4 years
    Here 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
    Rivas system almost 4 years
    Adding 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
    Luis Febro over 3 years
    This solution finally worked for me. Thanks!
  • dmitry.matora
    dmitry.matora over 3 years
    This 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
    Markus Schulz over 3 years
    looks 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
    andruso over 3 years
    Still the best choice in January 2021
  • amdev
    amdev over 3 years
    Pureeeeeeeee JS, Love it
  • Shoyeb Memon
    Shoyeb Memon over 3 years
    nice solution for a state component.
  • keemahs
    keemahs over 3 years
    so if i want to fire an event everytime value is set, will i do it like this ? - useEffect(() => { // function here }, [value]);
  • user3006381
    user3006381 over 3 years
    Worked 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
    Federico Budassi about 3 years
    The simplest one, by far!
  • TheEhsanSarshar
    TheEhsanSarshar about 3 years
    I just copy-pasted your solution without even thinking about it. and it's working like a charm 😋
  • Tony Scialo
    Tony Scialo about 3 years
    love this solution as it needs no new dependencies
  • Or Assayag
    Or Assayag about 3 years
    Prefect. Thanks.
  • Siva Kannan
    Siva Kannan about 3 years
    Please add comments for the downvoting, I have been using this code in my current app and its perfectly working
  • Brettins
    Brettins over 2 years
    Is this at all different from the original poster's solution?
  • Fiddle Freak
    Fiddle Freak over 2 years
    I 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
    acfasj over 2 years
    about 2019 example, what if searchFunction change?
  • Sebastien Lorber
    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
    Zhivko Zhelev over 2 years
    Yes, it's different here: debounce(handleChange , 200);
  • Muhammad Mubashirullah Durrani
    Muhammad Mubashirullah Durrani about 2 years
    This only sends the last letter entered
  • xji
    xji about 2 years
    Not sure what the issue was that you had with lodash @FiddleFreak, though one can also use a custom version of debounce function such as presented here: freecodecamp.org/news/javascript-debounce-example
  • Kostanos
    Kostanos about 2 years
    how do you cancel it if user component was unmounted?