How to debounce a callback in functional component using hooks

How can I get actual prop values in React Functional Component debounced callbacks, It worked in React Class Component, but I have no idea how to reach this behavior in functional component using hooks.

import React from "react";
import ReactDOM from "react-dom";
import debounce from "lodash.debounce";

const TestFunc = ({ count, onClick }) => {
  const handleClick = debounce(() => {
    onClick();
    console.log(count);
  }, 500);

  return (
    <div>
      <button type="button" onClick={handleClick}>
        Func: {count}
      </button>
    </div>
  );
};

class TestClass extends React.Component {
  handleClick = debounce(() => {
    this.props.onClick();
    console.log(this.props.count);
  }, 500);

  render() {
    return (
      <div>
        <button type="button" onClick={this.handleClick}>
          Class: {this.props.count}
        </button>
      </div>
    );
  }
}

const App = () => {
  const [countClass, setCountClass] = React.useState(0);
  const [countFunc, setCountFunc] = React.useState(0);

  return (
    <div>
      <TestFunc count={countFunc} onClick={() => setCountFunc(countFunc + 1)} />
      <TestClass
        count={countClass}
        onClick={() => setCountClass(countClass + 1)}
      />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

When you click on functional component button, it logs the previous count prop value to console, but it’s already changed by calling onClick handler, in the same time the class component button would log the actual count prop value after it was incremented by onClick handler. So, how can I get actual prop values in functional component?

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

Here’s a simple debounce hook (written in TypeScript)

import { useEffect, useRef } from "react";

export function useDebouncedCallback<A extends any[]>(
  callback: (...args: A) => void,
  wait: number
) {
  // track args & timeout handle between calls
  const argsRef = useRef<A>();
  const timeout = useRef<ReturnType<typeof setTimeout>>();

  function cleanup() {
    if(timeout.current) {
      clearTimeout(timeout.current);
    }
  }

  // make sure our timeout gets cleared if
  // our consuming component gets unmounted
  useEffect(() => cleanup, []);

  return function debouncedCallback(
    ...args: A
  ) {
    // capture latest args
    argsRef.current = args;

    // clear debounce timer
    cleanup();

    // start waiting again
    timeout.current = setTimeout(() => {
      if(argsRef.current) {
        callback(...argsRef.current);
      }
    }, wait);
  };
}

Example for your use case:
const handleClick = useDebouncedCallback(() => {
  onClick();
  console.log(count);
}, 500);

... 

<button type="button" onClick={handleClick}>
  Func: {count}
</button>

Also works for cases that pass arguments:
const handleChange = useDebouncedCallback((event) => {
  console.log(event.currentTarget.value);
}, 500);

<input onChange={handleChange}/>

Method 2

You need to make a few changes to use debounced method with hook

  1. You need to make use of the useCallback hook so that the debounced function is only created once on the initial render.
  2. Now if you have to make sure that debounced gets the correct count value when its executed, you need to pass it as a param else it will use the value from its enclosing closure at the time of its creation which is the initial count value.
  3. You need to update the count value on onClick method call using the callback pattern in parent like setCountFunc(count => count + 1) so that the child components re-render with the updated value

Working demo below

const TestFunc = ({ count, onClick }) => {
  const handleClick = React.useCallback((count) =>{
     const click = _.debounce((count) => {
          onClick();
          console.log(count);
     }, 500)
     click(count);
 }, []);

  console.log(count, 'render');
  return (
    <div>
      <button type="button" onClick={() => handleClick(count)}>
        Func: {count}
      </button>
    </div>
  );
};

class TestClass extends React.Component {
  handleClick = _.debounce(() => {
    this.props.onClick();
    console.log(this.props.count);
  }, 500);

  render() {
    return (
      <div>
        <button type="button" onClick={this.handleClick}>
          Class: {this.props.count}
        </button>
      </div>
    );
  }
}

const App = () => {
  const [countClass, setCountClass] = React.useState(0);
  const [countFunc, setCountFunc] = React.useState(0);

  return (
    <div>
      <TestFunc count={countFunc} onClick={() => setCountFunc(count => count + 1)} />
      <TestClass
        count={countClass}
        onClick={() => setCountClass(countClass + 1)}
      />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root" />

Method 3

for debounce API call in functional components

<input
 type="text"
 placeholder="Search"
 onChange={(e) => search(e.target.value)}
/>
=========================================================================
  const [typingTimeout, setTypingTimeout] = useState(0);
const search = async (value) => {
    if (typingTimeout) {
      clearTimeout(typingTimeout);
    }
    setTypingTimeout( setTimeout(() => {
        goToSearch(value);
      }, 1000)
    );

  }
  const goToSearch = async (value) => {
    const response = await getData(args);
  }

for class components
same as above but change in search function
const search = (event.target.value) =>{
    if (this.state.typingTimeout) {
      clearTimeout(this.state.typingTimeout);
    }
    this.setState({
      typingTimeout: setTimeout(()=> {
        this.goToSearch(event.target.value);
      }, 1000)
    });
}

for typeScript
setTypingTimeout( window.setTimeout(() => {
          goToSearch(value);
        }, 1000)
      );

use window.setTimeout


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x