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
  }
});

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.

const useSearchStarwarsHero = () => {
  // Handle the input text state
  const [inputText, setInputText] = useState('');

  // Debounce the original search async function
  const debouncedSearchStarwarsHero = useConstant(() =>
    AwesomeDebouncePromise(searchStarwarsHero, 300)
  );

  const search = useAsync(
    async () => {
      if (inputText.length === 0) {
        return [];
      } else {
        return debouncedSearchStarwarsHero(inputText);
      }
    },
    // Ensure a new request is made everytime the text changes (even if it's debounced)
    [inputText]
  );

  // Return everything needed for the hook consumer
  return {
    inputText,
    setInputText,
    search,
  };
};

And then you can use your hook:

const SearchStarwarsHeroExample = () => {
  const { inputText, setInputText, search } = useSearchStarwarsHero();
  return (
    <div>
      <input value={inputText} onChange={e => setInputText(e.target.value)} />
      <div>
        {search.loading && <div>...</div>}
        {search.error && <div>Error: {search.error.message}</div>}
        {search.result && (
          <div>
            <div>Results: {search.result.length}</div>
            <ul>
              {search.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.


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.


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!


If all you need from the event object is to get the DOM input element, the solution is much simpler – just use ref. Note that this requires Underscore:

class Item extends React.Component {
    constructor(props) {
        super(props);
        this.saveTitle = _.throttle(this.saveTitle.bind(this), 1000);
    }
    saveTitle(){
        let val = this.inputTitle.value;
        // make the ajax call
    }
    render() {
        return <input 
                    ref={ el => this.inputTitle = el } 
                    type="text" 
                    defaultValue={this.props.title} 
                    onChange={this.saveTitle} />
    }
}

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}
        />
    )
  }
}

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.


With debounce you need to keep the original synthetic event around with event.persist(). Here is working example tested with React 16+.

import React, { Component } from 'react';
import debounce from 'lodash/debounce'

class ItemType extends Component {

  evntHandler = debounce((e) => {
    console.log(e)
  }, 500);

  render() {
    return (
      <div className="form-field-wrap"
      onClick={e => {
        e.persist()
        this.evntHandler(e)
      }}>
        ...
      </div>
    );
  }
}
export default ItemType;

With functional component, you can do this -

const Search = ({ getBooks, query }) => {

  const handleOnSubmit = (e) => {
    e.preventDefault();
  }
  const debouncedGetBooks = debounce(query => {
    getBooks(query);
  }, 700);

  const onInputChange = e => {
    debouncedGetBooks(e.target.value)
  }

  return (
    <div className="search-books">
      <Form className="search-books--form" onSubmit={handleOnSubmit}>
        <Form.Group controlId="formBasicEmail">
          <Form.Control type="text" onChange={onInputChange} placeholder="Harry Potter" />
          <Form.Text className="text-muted">
            Search the world's most comprehensive index of full-text books.
          </Form.Text>
        </Form.Group>
        <Button variant="primary" type="submit">
          Search
        </Button>
      </Form>
    </div>
  )
}

References - - https://gist.github.com/elijahmanor/08fc6c8468c994c844213e4a4344a709 - https://blog.revathskumar.com/2016/02/reactjs-using-debounce-in-react-components.html


If you are using redux you can do this in a very elegant way with middleware. You can define a Debounce middleware as:

var timeout;
export default store => next => action => {
  const { meta = {} } = action;
  if(meta.debounce){
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      next(action)
    }, meta.debounce)
  }else{
    next(action)
  }
}

You can then add debouncing to action creators, such as:

export default debouncedAction = (payload) => ({
  type : 'DEBOUNCED_ACTION',
  payload : payload,
  meta : {debounce : 300}
}

There's actually already middleware you can get off npm to do this for you.


Lots of good info here already, but to be succinct. This works for me...

import React, {Component} from 'react';
import _ from 'lodash';

class MyComponent extends Component{
      constructor(props){
        super(props);
        this.handleChange = _.debounce(this.handleChange.bind(this),700);
      }; 

Using ES6 CLASS and React 15.x.x & lodash.debounce Im using React's refs here since event losses the this bind internally.

class UserInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      userInput: ""
    };
    this.updateInput = _.debounce(this.updateInput, 500);
  }


  updateInput(userInput) {
    this.setState({
      userInput
    });
    //OrderActions.updateValue(userInput);//do some server stuff
  }


  render() {
    return ( <div>
      <p> User typed: {
        this.state.userInput
      } </p>
      <input ref = "userValue" onChange = {() => this.updateInput(this.refs.userValue.value) } type = "text" / >
      </div>
    );
  }
}

ReactDOM.render( <
  UserInput / > ,
  document.getElementById('root')
);
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>


<div id="root"></div>


You can use Lodash debounce https://lodash.com/docs/4.17.5#debounce method. It is simple and effective.

import * as lodash from lodash;

const update = (input) => {
    // Update the input here.
    console.log(`Input ${input}`);     
}

const debounceHandleUpdate = lodash.debounce((input) => update(input), 200, {maxWait: 200});

doHandleChange() {
   debounceHandleUpdate(input);
}

You can also cancel the debounce method by using the below method.

this.debounceHandleUpdate.cancel();

Hope it helps you. Cheers!!


FYI

Here is another PoC implementation:

  • without any libraries (e.g. lodash) for debouncing
  • using React Hooks API

I hope it helps :)

import React, { useState, useEffect, ChangeEvent } from 'react';

export default function DebouncedSearchBox({
  inputType,
  handleSearch,
  placeholder,
  debounceInterval,
}: {
  inputType?: string;
  handleSearch: (q: string) => void;
  placeholder: string;
  debounceInterval: number;
}) {
  const [query, setQuery] = useState<string>('');
  const [timer, setTimer] = useState<NodeJS.Timer | undefined>();

  useEffect(() => {
    if (timer) {
      clearTimeout(timer);
    }
    setTimer(setTimeout(() => {
      handleSearch(query);
    }, debounceInterval));
  }, [query]);

  const handleOnChange = (e: ChangeEvent<HTMLInputElement>): void => {
    setQuery(e.target.value);
  };

  return (
    <input
      type={inputType || 'text'}
      className="form-control"
      placeholder={placeholder}
      value={query}
      onChange={handleOnChange}
    />
  );
}

Just another variant with recent react and lodash.

class Filter extends Component {
  static propTypes = {
    text: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired
  }

  state = {
    initialText: '',
    text: ''
  }

  constructor (props) {
    super(props)

    this.setText = this.setText.bind(this)
    this.onChange = _.fp.debounce(500)(this.onChange.bind(this))
  }

  static getDerivedStateFromProps (nextProps, prevState) {
    const { text } = nextProps

    if (text !== prevState.initialText) {
      return { initialText: text, text }
    }

    return null
  }

  setText (text) {
    this.setState({ text })
    this.onChange(text)
  }

  onChange (text) {
    this.props.onChange(text)
  }

  render () {
    return (<input value={this.state.text} onChange={(event) => this.setText(event.target.value)} />)
  }
}

There's a use-debounce package that you can use with ReactJS hooks.

From package's README:

import { useDebounce } from 'use-debounce';

export default function Input() {
  const [text, setText] = useState('Hello');
  const [value] = useDebounce(text, 1000);

  return (
    <div>
      <input
        defaultValue={'Hello'}
        onChange={(e) => {
          setText(e.target.value);
        }}
      />
      <p>Actual value: {text}</p>
      <p>Debounce value: {value}</p>
    </div>
  );
}

As you can see from the example above, it is set up to update the variable value only once every second (1000 milliseconds).


Did you try?

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) {
    debounce(\\ Your handleChange code , 200);
  }
});

Instead of wrapping the handleOnChange in a debounce(), why not wrap the ajax call inside the callback function inside the debounce, thereby not destroying the event object. So something like this:

handleOnChange: function (event) {
   debounce(
     $.ajax({})
  , 250);
}

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} />
    }
}

A nice and clean solution, that doesn't require any external dependencies:

Debouncing with React Hooks

It uses a custom plus the useEffect React hooks and the setTimeout / clearTimeout method.


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

Example:

enter image description here

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


I was searching for a solution to the same problem and came across this thread as well as some others but they had the same problem: if you are trying to do a handleOnChange function and you need the value from an event target, you will get cannot read property value of null or some such error. In my case, I also needed to preserve the context of this inside the debounced function since I'm executing a fluxible action. Here's my solution, it works well for my use case so I'm leaving it here in case anyone comes across this thread:

// at top of file:
var myAction = require('../actions/someAction');

// inside React.createClass({...});

handleOnChange: function (event) {
    var value = event.target.value;
    var doAction = _.curry(this.context.executeAction, 2);

    // only one parameter gets passed into the curried function,
    // so the function passed as the first parameter to _.curry()
    // will not be executed until the second parameter is passed
    // which happens in the next function that is wrapped in _.debounce()
    debouncedOnChange(doAction(myAction), value);
},

debouncedOnChange: _.debounce(function(action, value) {
    action(value);
}, 300)

for throttle or debounce the best way is to create a function creator so you can use it any where, for example:

  updateUserProfileField(fieldName) {
    const handler = throttle(value => {
      console.log(fieldName, value);
    }, 400);
    return evt => handler(evt.target.value.trim());
  }

and in your render method you can do:

<input onChange={this.updateUserProfileField("givenName").bind(this)}/>

the updateUserProfileField method will create a separated function each time you call it.

Note don't try to return the handler directly for example this will not work:

 updateUserProfileField(fieldName) {
    return evt => throttle(value => {
      console.log(fieldName, value);
    }, 400)(evt.target.value.trim());
  }

the reason why this will not work because this will generate a new throttle function each time the event called instead of using the same throttle function, so basically the throttle will be useless ;)

Also if you use debounce or throttle you don't need setTimeout or clearTimeout, this is actually why we use them :P


Here's a snippet using @Abra's approach wrapped in a function component (we use fabric for the UI, just replace it with a simple button)

import React, { useCallback } from "react";
import { debounce } from "lodash";

import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';

const debounceTimeInMS = 2000;

export const PrimaryButtonDebounced = (props) => {

    const debouncedOnClick = debounce(props.onClick, debounceTimeInMS, { leading: true });

    const clickHandlerDebounced = useCallback((e, value) => {

        debouncedOnClick(e, value);

    },[]);

    const onClick = (e, value) => {

        clickHandlerDebounced(e, value);
    };

    return (
        <PrimaryButton {...props}
            onClick={onClick}
        />
    );
}

You can also use a self-written mixin, something like this:

var DebounceMixin = {
  debounce: function(func, time, immediate) {
    var timeout = this.debouncedTimeout;
    if (!timeout) {
      if (immediate) func();
      this.debouncedTimeout = setTimeout(function() {
        if (!immediate) func();
        this.debouncedTimeout = void 0;
      }.bind(this), time);
    }
  }
};

And then use it in your component like this:

var MyComponent = React.createClass({
  mixins: [DebounceMixin],
  handleClick: function(e) {
    this.debounce(function() {
      this.setState({
        buttonClicked: true
      });
    }.bind(this), 500, true);
  },
  render: function() {
    return (
      <button onClick={this.handleClick}></button>
    );
  }
});

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)
     });
 }

a little late here but this should help. create this class(its written in typescript but its easy to convert it to javascript)

export class debouncedMethod<T>{
  constructor(method:T, debounceTime:number){
    this._method = method;
    this._debounceTime = debounceTime;
  }
  private _method:T;
  private _timeout:number;
  private _debounceTime:number;
  public invoke:T = ((...args:any[])=>{
    this._timeout && window.clearTimeout(this._timeout);
    this._timeout = window.setTimeout(()=>{
      (this._method as any)(...args);
    },this._debounceTime);
  }) as any;
}

and to use

var foo = new debouncedMethod((name,age)=>{
 console.log(name,age);
},500);
foo.invoke("john",31);

you can use tlence tlence

function log(server) {
  console.log('connecting to', server);
}

const debounceLog = debounce(log, 5000);
// just run last call to 5s
debounceLog('local');
debounceLog('local');
debounceLog('local');
debounceLog('local');
debounceLog('local');
debounceLog('local');

Julen solution is kind of hard to read, here's clearer and to-the-point react code for anyone who stumbled him based on title and not the tiny details of the question.

tl;dr version: when you would update to observers send call a schedule method instead and that in turn will actually notify the observers (or perform ajax, etc)

Complete jsfiddle with example component jsfiddle

var InputField = React.createClass({

    getDefaultProps: function () {
        return {
            initialValue: '',
            onChange: null
        };
    },

    getInitialState: function () {
        return {
            value: this.props.initialValue
        };
    },

    render: function () {
        var state = this.state;
        return (
            <input type="text"
                   value={state.value}
                   onChange={this.onVolatileChange} />
        );
    },

    onVolatileChange: function (event) {
        this.setState({ 
            value: event.target.value 
        });

        this.scheduleChange();
    },

    scheduleChange: _.debounce(function () {
        this.onChange();
    }, 250),

    onChange: function () {
        var props = this.props;
        if (props.onChange != null) {
            props.onChange.call(this, this.state.value)
        }
    },

});

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:

  1. The callback with no debouncing
  2. Calls a debounced function with only the pieces of the event you need (so the synthetic event can be recycled)

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).


Extend useState hook

import { useState } from "react";
import _ from "underscore"
export const useDebouncedState = (initialState, durationInMs = 500) => {
    const [internalState, setInternalState] = useState(initialState);
    const debouncedFunction = _.debounce(setInternalState, durationInMs);
    return [internalState, debouncedFunction];
};
export default useDebouncedState;

Use hook

import useDebouncedState from "../hooks/useDebouncedState"
//...
const [usernameFilter, setUsernameFilter] = useDebouncedState("")
//...
<input id="username" type="text" onChange={e => setUsernameFilter(e.target.value)}></input>

https://trippingoncode.com/react-debounce-hook/


Met this problem today. Solved it using setTimeout and clearTimeout.

I will give an example that you could adapt:

import React, { Component } from 'react'

const DEBOUNCE_TIME = 500

class PlacesAutocomplete extends Component {
  debounceTimer = null;

  onChangeHandler = (event) => {
    // Clear the last registered timer for the function
    clearTimeout(this.debounceTimer);

    // Set a new timer
    this.debounceTimer = setTimeout(
      // Bind the callback function to pass the current input value as arg
      this.getSuggestions.bind(null, event.target.value), 
      DEBOUNCE_TIME
    )
  }

  // The function that is being debounced
  getSuggestions = (searchTerm) => {
    console.log(searchTerm)
  }

  render() {
    return (
      <input type="text" onChange={this.onChangeHandler} />
    )
  }
}

export default PlacesAutocomplete

You could also refactor it in it's own function component:

import React from 'react'

function DebouncedInput({ debounceTime, callback}) {
  let debounceTimer = null
  return (
    <input type="text" onChange={(event) => {
      clearTimeout(debounceTimer);

      debounceTimer = setTimeout(
        callback.bind(null, event.target.value), 
        debounceTime
      )
    }} />
  )
}

export default DebouncedInput

And use it like:

import React, { Component } from 'react'
import DebouncedInput from '../DebouncedInput';

class PlacesAutocomplete extends Component {
  debounceTimer = null;

  getSuggestions = (searchTerm) => {
    console.log(searchTerm)
  }

  render() {
    return (
      <DebouncedInput debounceTime={500} callback={this.getSuggestions} />
    )
  }
}

export default PlacesAutocomplete

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}  />

My solution is hooks based (written in Typescript).

I've got 2 main hooks useDebouncedValue and useDebouncedCallback

First - useDebouncedValue

Let's say we've got a search box, but we want to ask the server for search results after the user has stopped typing for 0,5s

function SearchInput() {
  const [realTimeValue, setRealTimeValue] = useState('');

  const debouncedValue = useDebouncedValue(realTimeValue, 500); // this value will pick real time value, but will change it's result only when it's seattled for 500ms

  useEffect(() => {
    // this effect will be called on seattled values
    api.fetchSearchResults(debouncedValue);
  }, [debouncedValue])

  return <input onChange={event => setRealTimeValue(event.target.value)} />
}

Implementation

import { useState, useEffect } from "react";

export function useDebouncedValue<T>(input: T, time = 500) {
  const [debouncedValue, setDebouncedValue] = useState(input);

  // every time input value has changed - set interval before it's actually commited
  useEffect(() => {
    const timeout = setTimeout(() => {
      setDebouncedValue(input);
    }, time);

    return () => {
      clearTimeout(timeout);
    };
  }, [input, time]);

  return debouncedValue;
}

Second useDebouncedCallback

It just creates a 'debounced' function in the scope of your component.

Let's say we've got a component with a button that will show alert 500ms after you stopped clicking it.

function AlertButton() {
  function showAlert() {
    alert('Clicking has seattled');
  }

  const debouncedShowAlert = useDebouncedCallback(showAlert, 500);

  return <button onClick={debouncedShowAlert}>Click</button>
}

Implementation (note I'm using lodash/debounce as a helper)

import debounce from 'lodash/debounce';
import { useMemo } from 'react';

export function useDebouncedCallback<T extends (...args: any) => any>(callback: T, wait?: number) {
  const debouncedCallback = useMemo(() => debounce(callback, wait), [callback, wait]);

  return debouncedCallback;
}