Chetan Ankola
Chetan Ankola

Reputation: 7035

How can I perform a debounce?

How do you perform debounce in React?

I want to debounce the handleOnChange function.

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

Upvotes: 696

Views: 525035

Answers (30)

Gagandeep Gambhir
Gagandeep Gambhir

Reputation: 4353

2024 - NPM packages for debouncing

To contribute the latest information to this thread, I'd like to share the following npm packages that facilitate achieving debouncing:

  1. use-debounce
  2. debounce

Upvotes: -1

Sebastien Lorber
Sebastien Lorber

Reputation: 92112

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 the 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.js) 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.js 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: three instances are producing one log entry per instance (that makes three 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: three instances are producing only one 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 second (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 second (async) will print hasNativeEvent=true, because persist allows you to avoid putting the event back in the pool.

You can test these two behaviors here: JSFiddle

Read Julen's answer for an example of using persist() with a throttle/debounce function.

Upvotes: 994

Francisco Hanna
Francisco Hanna

Reputation: 1137

I met this problem today and 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 the argument
      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 its 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

Upvotes: 2

Zhivko Zhelev
Zhivko Zhelev

Reputation: 343

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

Upvotes: 3

anaval
anaval

Reputation: 1138

Create this class (it’s written in TypeScript, but it’s 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);

Upvotes: 0

kenju
kenju

Reputation: 5944

FYI

Here is another PoC implementation:

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

Upvotes: 7

Behnam
Behnam

Reputation: 6459

You can use 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');

Upvotes: -1

puchu
puchu

Reputation: 3652

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

Upvotes: 4

Dinesh Madanlal
Dinesh Madanlal

Reputation: 347

You can use the Lodash 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();

Upvotes: 8

STEEL
STEEL

Reputation: 10007

Using ES6 CLASS, React 15.x.x, and lodash.debounce. I'm using React's refs here since the event loses 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>

Upvotes: 8

Hooman Askari
Hooman Askari

Reputation: 1626

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.

Upvotes: 27

Fareed Alnamrouti
Fareed Alnamrouti

Reputation: 32154

For throttle or debounce, the best way is to create a function creator, so you can use it anywhere, 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 is 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

Upvotes: 1

Matt
Matt

Reputation: 4049

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.

Upvotes: 9

julen
julen

Reputation: 4979

Uncontrolled Components

You can use the event.persist() method.

An example follows using Underscore.js' _.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} />
    );
  }

});

See this JSFiddle.


Controlled Components

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

Upvotes: 233

srcspider
srcspider

Reputation: 11205

Julen's solution is kind of hard to read. Here's clearer and to-the-point react code for anyone who stumbled upon that answer based on the 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)
        }
    },

});

Upvotes: 0

Robert
Robert

Reputation: 63

Instead of wrapping the handleOnChange in a debounce(), 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);
}

Upvotes: 3

superluminary
superluminary

Reputation: 49132

2022 - use a useEffect hook

Your best option at this time is to use the useEffect hook. useEffect lets you set a function that can modify state in response to some async event. Debouncing is asynchronous, so useEffect works nicely for this purpose.

If you return a function from the hook, the returned function will be called before the hook is called again. This lets you cancel the previous timeout, effectively debouncing the function.

Example

Here we have two states, value and tempValue. Setting tempValue will trigger a useEffect hook that will start a 1000 ms timeout which will call a function to copy tempValue into value.

The hook returns a function that unsets the timer. When the hook is called again (i.e., another key is pressed) the timeout is canceled and reset.

const DebounceDemo = () => {
  const [value, setValue] = useState();
  const [tempValue, setTempValue] = useState();

  // This hook will set a 1000 ms timer to copy tempValue into value
  // If the hook is called again, the timer will be cancelled
  // This creates a debounce
  useEffect(
    () => {
      // Wait 1000 ms before copying the value of tempValue into value;
      const timeout = setTimeout(() => {
        setValue(tempValue);
      }, 1000);

      // If the hook is called again, cancel the previous timeout
      // This creates a debounce instead of a delay
      return () => clearTimeout(timeout);
    },
    // Run the hook every time the user makes a keystroke
    [tempValue]
  )

  // Here we create an input to set tempValue.
  // value will be updated 1000 ms after the hook is last called,
  // i.e after the last user keystroke.
  return (
    <>
      <input
        onChange={
          ({ target }) => setTempValue(target.value)
        }
      />
      <p>{ value }</p>
    </>
  )
}

Upvotes: 20

Or Assayag
Or Assayag

Reputation: 6336

As of June 2021, you can simply implement xnimorz's solution: use-debounce

import { useState, useEffect, useRef } from "react";
// Usage
function App() {
  // State and setters for ...
  // Search term
  const [searchTerm, setSearchTerm] = useState("");
  // API search results
  const [results, setResults] = useState([]);
  // Searching status (whether there is pending API request)
  const [isSearching, setIsSearching] = useState(false);
  // Debounce search term so that it only gives us latest value ...
  // ... if searchTerm has not been updated within last 500 ms.
  // The goal is to only have the API call fire when user stops typing ...
  // ... so that we aren't hitting our API rapidly.
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  // Effect for API call
  useEffect(
    () => {
      if (debouncedSearchTerm) {
        setIsSearching(true);
        searchCharacters(debouncedSearchTerm).then((results) => {
          setIsSearching(false);
          setResults(results);
        });
      } else {
        setResults([]);
        setIsSearching(false);
      }
    },
    [debouncedSearchTerm] // Only call effect if debounced search term changes
  );
  return (
    <div>
      <input
        placeholder="Search Marvel Comics"
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {isSearching && <div>Searching ...</div>}
      {results.map((result) => (
        <div key={result.id}>
          <h4>{result.title}</h4>
          <img
            src={`${result.thumbnail.path}/portrait_incredible.${result.thumbnail.extension}`}
          />
        </div>
      ))}
    </div>
  );
}
// API search function
function searchCharacters(search) {
  const apiKey = "f9dfb1e8d466d36c27850bedd2047687";
  return fetch(
    `https://gateway.marvel.com/v1/public/comics?apikey=${apiKey}&titleStartsWith=${search}`,
    {
      method: "GET",
    }
  )
    .then((r) => r.json())
    .then((r) => r.data.results)
    .catch((error) => {
      console.error(error);
      return [];
    });
}
// Hook
function useDebounce(value, delay) {
  // State and setters for debounced value
  const [debouncedValue, setDebouncedValue] = useState(value);
  useEffect(
    () => {
      // Update debounced value after delay
      const handler = setTimeout(() => {
        setDebouncedValue(value);
      }, delay);
      // Cancel the timeout if value changes (also on delay change or unmount)
      // This is how we prevent debounced value from updating if value is changed ...
      // .. within the delay period. Timeout gets cleared and restarted.
      return () => {
        clearTimeout(handler);
      };
    },
    [value, delay] // Only recall effect if value or delay changes
  );
  return debouncedValue;
}

Upvotes: 6

Coding Elements
Coding Elements

Reputation: 1038

You have to use useCallback as mentioned in the blog post How to use debounce and throttle in React and abstract them into hooks.

import React, { useCallback } from 'react';
import debounce from 'debounce'; // Or another package

function App() {
    ...
    const debouncedSave = useCallback(
        debounce(x => foo(x), 1000),
        [], // Will be created only once initially
    );
    ...
}

Upvotes: 0

Shubhra Kushal
Shubhra Kushal

Reputation: 309

If we are interested in using epic.js with React, we could use the debounceTime RxJS/operator with the epic.js library. Hence instead of using callbacks, we could use observables in React with the help of epic.js.

Upvotes: -1

Siva Kannan
Siva Kannan

Reputation: 2461

Simple and effective: Use use-debounce

import { useDebouncedCallback } from 'use-debounce';

function Input({ defaultValue }) {
  const [value, setValue] = useState(defaultValue);
  const debounced = useDebouncedCallback(
    (value) => {
      setValue(value);
    },
    // Delay
    1000
  );

  return (
    <div>
      <input defaultValue={defaultValue} onChange={(e) => debounced(e.target.value)} />
      <p>Debounced value: {value}</p>
    </div>
  );
}

Upvotes: 2

Xinan
Xinan

Reputation: 3162

I can't find any answers under this question mentioning the approach I am using, so I just want to provide an alternative solution here which I think is the best for my use case.

If you are using the popular React hooks toolkit library called react-use, then there is a utility hook called useDebounce() that implemented denounce logic in a quite elegant way.

const [query, setQuery] = useState('');

useDebounce(
  () => {
    emitYourOnDebouncedSearchEvent(query);
  },
  2000,
  [query]
);

return <input onChange={({ currentTarget }) => setQuery(currentTarget.value)} />

For details, please check the library's GitHub page directly.

Upvotes: 6

Vince
Vince

Reputation: 869

If you just need to perform a debounce in a button for requesting data, the code provided might be helpful to you:

  1. Create a function to prevent the default with conditional statement if requesting is true or false

  2. Implement the useState Hook and useEffect Hook

    const PageOne = () => {
     const [requesting, setRequesting] = useState(false);
    
      useEffect(() => {
        return () => {
          setRequesting(false);
        };
      }, [requesting]);
    
      const onDebounce = (e) => {
        if (requesting === true) {
          e.preventDefault();
        }
        // ACTIONS
        setLoading(true);
      };
    
     return (
      <div>
    
        <button onClick={onDebounce}>Requesting data</button>
      </div>
     )
    }
    

Upvotes: 1

Ievgen
Ievgen

Reputation: 4443

React Ajax debounce and cancellation example solution using React hooks and reactive programming (RxJS):

import React, { useEffect, useState } from "react";
import { ajax } from "rxjs/ajax";
import { debounceTime, delay, takeUntil } from "rxjs/operators";
import { Subject } from "rxjs/internal/Subject";

const App = () => {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);
  const [filterChangedSubject] = useState(() => {
    // Arrow function is used to init Singleton Subject (in a scope of a current component)
    return new Subject<string>();
  });

  useEffect(() => {
    // Effect that will be initialized once on a React component init.
    const subscription = filterChangedSubject
      .pipe(debounceTime(200))
      .subscribe((filter) => {
        if (!filter) {
          setLoading(false);
          setItems([]);
          return;
        }
        ajax(`https://swapi.dev/api/people?search=${filter}`)
          .pipe(
            // Current running Ajax is cancelled on filter change.
            takeUntil(filterChangedSubject)
          )
          .subscribe(
            (results) => {
              // Set items will cause render:
              setItems(results.response.results);
            },
            () => {
              setLoading(false);
            },
            () => {
              setLoading(false);
            }
          );
      });

    return () => {
      // On Component destroy. notify takeUntil to unsubscribe from current running Ajax request
      filterChangedSubject.next("");
      // Unsubscribe filter change listener
      subscription.unsubscribe();
    };
  }, []);

  const onFilterChange = (e) => {
    // Notify subject about the filter change
    filterChangedSubject.next(e.target.value);
  };
  return (
    <div>
      Cards
      {loading && <div>Loading...</div>}
      <input onChange={onFilterChange}></input>
      {items && items.map((item, index) => <div key={index}>{item.name}</div>)}
    </div>
  );
};

export default App;

Upvotes: 1

tomatentobi
tomatentobi

Reputation: 3157

If you don't like to add Lodash or any other package:

import React, { useState, useRef } from "react";

function DebouncedInput() {
  const [isRefetching, setIsRefetching] = useState(false);
  const [searchTerm, setSearchTerm] = useState("");
  const previousSearchTermRef = useRef("");

  function setDebouncedSearchTerm(value) {
    setIsRefetching(true);
    setSearchTerm(value);
    previousSearchTermRef.current = value;
    setTimeout(async () => {
      if (previousSearchTermRef.current === value) {
        try {
          // await refetch();
        } finally {
          setIsRefetching(false);
        }
      }
    }, 500);
  }

  return (
    <input
      value={searchTerm}
      onChange={(event) => setDebouncedSearchTerm(event.target.value)}
    />
  );
}

Upvotes: -1

Adam Pietrasiak
Adam Pietrasiak

Reputation: 13184

My solution is hooks-based (written in TypeScript).

I've got two 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.5 seconds:

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 the input value has changed - set interval before it's actually committed
  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 an alert 500 ms 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;
}

Upvotes: 21

user1079877
user1079877

Reputation: 9358

This solution does not need any extra libraries, 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}  />

Upvotes: 2

Ashutosh Tripathi
Ashutosh Tripathi

Reputation: 291

There can be a simple approach using React hooks.

Step 1: define a state to maintain searched text

const [searchTerm, setSearchTerm] = useState('')

Step 2: Use useEffect to capture any change in searchTerm

useEffect(() => {
  const delayDebounceFn = setTimeout(() => {
    if (searchTerm) {
      // Write your logic here
    }
  }, 400)

  return () => clearTimeout(delayDebounceFn)
}, [searchTerm])

Step 3: Write a function to handle input change

function handleInputChange(value) {
  if (value) {
    setSearchTerm(value)
  }
}

That's all! Call this method as and when required.

Upvotes: 22

Marcio J
Marcio J

Reputation: 780

This is the cleanest way I have found to accomplish debouncing. I know there are similar answers, but I would like to document the bare idea, so anyone in the future can grasp and fit in their code.

let timer = null;
function debouce(func) {
  clearTimeout(timer);
  timer = setTimeout(() => {
    func();
  }, 1000);
}

// simulate frequent calls
debouce(()=> console.log("call 1"));
debouce(()=> console.log("call 2"));
debouce(()=> console.log("call 3"));

// simulate future call
setTimeout(() => {
  debouce(()=> console.log("call 4"));
}, 2000);

Upvotes: 0

Sameer Ingavale
Sameer Ingavale

Reputation: 2210

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

Upvotes: 132

Related Questions