Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 761
zalishchuk Avatar asked May 23 '19 23:05

zalishchuk


2 Answers

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}/>
like image 81
bingles Avatar answered Nov 09 '22 08:11

bingles


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" />
like image 23
Shubham Khatri Avatar answered Nov 09 '22 09:11

Shubham Khatri