3

I would like to use lodash debounce to prevent a button onClick from being run more than once in the event of multiple, rapid clicks. The button is in stateless functional component and from what I've read (in this SO question), debounce will not work in functional components due the debounced function being new each render.

One way to solve this seems to be to use the useCallback react hook but, unfortunately, the project is currently at react version 15.6.1 and it is not feasible to upgrade at this time. I can also get debounce to work if I convert the component to a stateful component or if I pass in an already debounced submit function, but I think the best case outcome would be maintaining the functional component while still having it be able to protect against multiple submissions for any submit function.

Is it possible to get debounce to work inside a functional component without using hooks?

Here is my functional component for which debounce isn't working:

import * as React from 'react';
import * as Lodash from 'lodash';
import { Button } from 'react-bootstrap';
import { Modal } from 'react-bootstrap';

export const ConfirmSubmitModal = props => {
    const { submit } = props;

    const debouncedSubmit = Lodash.debounce(() => {
        submit();
    }, 3000, { "leading": true, "trailing": false });

    return (
        <div>
            <Modal>
                  <Button onClick={debouncedSubmit}>Submit</Button>
            </Modal>
        </div>
    );
};
John Henderson
  • 115
  • 2
  • 8
  • `submit` is being passed through props, would it be feasible to debounce it before passing it down to `ConfirmSubmitModal`? – schu34 Nov 26 '19 at 23:16
  • @schu34 it is feasible and that does work. In some cases though, I think it could be better to have the debouncing done inside the submit component so that the user of the component doesn't have to replicate the debouncing code or remember to debounce the submit function. Though maybe that is the best way to do it and the potential code duplication can be handled in another way. – John Henderson Nov 26 '19 at 23:27

1 Answers1

3

You can create something similar to useCallback hook using _.memoize(). As long as the submit function passed via the props won't change, getDebouncedSubmit will return the same debounced function.

_.memoize.Cache = WeakMap; // use a weak map as _.memoize cache to prevent memory leaks

const getDebouncedSubmit = _.memoize(submit => 
  _.debounce(() => {
    submit();
  }, 3000, { "leading": true, "trailing": false })
);

const ConfirmSubmitModal = ({ submit }) => {
  const debouncedSubmit = getDebouncedSubmit(submit);

  return (
    <div>
      <button onClick={debouncedSubmit}>Submit</button>
    </div>
  );
};

const submit = () => console.log('clicked');

ReactDOM.render(
  <ConfirmSubmitModal submit={submit} />,
  demo
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>


<div id="demo"></div>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • 1
    I ended up just using a stateful class, as I had difficulties with getting the modal to close with this approach (perhaps due to something else in my app). That said, this seems like a good solution in general, thank you! – John Henderson Nov 27 '19 at 17:03
  • I've updated the solution with a WeakMap as the memoize cache. – Ori Drori Nov 27 '19 at 18:07