46

I'm trying to add debouncing with lodash to a search function, called from an input onChange event. The code below generates a type error 'function is expected', which I understand because lodash is expecting a function. What is the right way to do this and can it be done all inline? I have tried nearly every example thus far on SO to no avail.

search(e){
 let str = e.target.value;
 debounce(this.props.relay.setVariables({ query: str }), 500);
},
naXa stands with Ukraine
  • 35,493
  • 19
  • 190
  • 259
Michael Kaufman
  • 683
  • 1
  • 9
  • 19
  • try this https://www.freecodecamp.org/news/debounce-and-throttle-in-react-with-hooks/ – U.A Jun 01 '21 at 05:01

13 Answers13

68

With a functional react component try using useCallback. useCallback memoizes your debounce function so it doesn't get recreated again and again when the component rerenders. Without useCallback the debounce function will not sync with the next key stroke.

`

import {useCallback} from 'react';
import _debounce from 'lodash/debounce';
import axios from 'axios';

function Input() {
    const [value, setValue] = useState('');

    const debounceFn = useCallback(_debounce(handleDebounceFn, 1000), []);

    function handleDebounceFn(inputValue) {
        axios.post('/endpoint', {
          value: inputValue,
        }).then((res) => {
          console.log(res.data);
        });
    }


    function handleChange (event) {
        setValue(event.target.value);
        debounceFn(event.target.value);
    };

    return <input value={value} onChange={handleChange} />
}

`

Duc Filan
  • 6,769
  • 3
  • 21
  • 26
Jules Patry
  • 789
  • 4
  • 4
  • you have a typo in the import: import _debouce from 'lodash/debounce'; It is NOT _debouce IT IS _debounce – Juanma Menendez Sep 11 '21 at 17:01
  • Thank you jules! – Ashfaq nisar Dec 17 '21 at 18:49
  • just a warning, `useCallback` will set all variables with `useState` to the initial value of the page loaded. I ran into this bug, and am now trying to find another way to use the debouncer. – Fiddle Freak Dec 27 '21 at 21:29
  • @FiddleFreak I have never experienced that. I think the issue is something else. – Jules Patry Dec 30 '21 at 02:52
  • @JulesPatry I actually got around that problem by doing this instead > https://stackoverflow.com/questions/70501416/debug-usecallback-with-debouncer-resets-usestate-variables – Fiddle Freak Dec 30 '21 at 06:26
  • make sure you call the handler as a function rather than assign to const handleDebounceFn = () because it will not hoist the variable above the declaration, but it will for an explicit function. – Kai Durai Aug 28 '23 at 20:39
42

The debounce function can be passed inline in the JSX or set directly as a class method as seen here:

search: _.debounce(function(e) {
  console.log('Debounced Event:', e);
}, 1000)

Fiddle: https://jsfiddle.net/woodenconsulting/69z2wepo/36453/

If you're using es2015+ you can define your debounce method directly, in your constructor or in a lifecycle method like componentWillMount.

Examples:

class DebounceSamples extends React.Component {
  constructor(props) {
    super(props);

    // Method defined in constructor, alternatively could be in another lifecycle method
    // like componentWillMount
    this.search = _.debounce(e => {
      console.log('Debounced Event:', e);
    }, 1000);
  }

  // Define the method directly in your class
  search = _.debounce((e) => {
    console.log('Debounced Event:', e);
  }, 1000)
}
Jeff Wooden
  • 5,339
  • 2
  • 19
  • 24
  • 2
    Thanks for that. What I'm seeing now is a console log of the synthetic event and I need the e.target.value to perform the search.. I've tried e.persist() but it doesn't seem to do anything. Debouncing is technically working but without passing it a value it's not working. Thanks for any help. – Michael Kaufman Mar 30 '16 at 00:23
  • I couldn't use that exactly, but it got me where I needed to go. I basically had the input call search(e) and then passed that event to another function with the debouncing. I read about event.persist() but I couldn't get that work. Thanks for your help!! – Michael Kaufman Mar 30 '16 at 04:41
  • @Jeff Wooden fidden is broken – ey dee ey em Oct 18 '17 at 03:47
  • thanks for suggesting componentWillMount. able to access props function as well in debounced function. if i put inside constructor, somehow i am not able to access props functions. – Rajesh Mbm May 05 '18 at 20:21
  • @RajeshMbm You can access props inside of a class constructor, see the updated example - it's available as the first argument (make sure to include the super call). – Jeff Wooden May 08 '18 at 14:05
  • it would be nice if we could see a solution using react hooks. – Fiddle Freak Dec 27 '21 at 21:27
22

This is how I had to do it after googling the whole day.

const MyComponent = (props) => {
  const [reload, setReload] = useState(false);

  useEffect(() => {
    if(reload) { /* Call API here */ }
  }, [reload]);

  const callApi = () => { setReload(true) }; // You might be able to call API directly here, I haven't tried
  const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));

  function handleChange() { 
    debouncedCallApi(); 
  }

  return (<>
    <input onChange={handleChange} />
  </>);
}
Aximili
  • 28,626
  • 56
  • 157
  • 216
  • useEffect trigger only one time, because reload after first call will be always true. – CuteShaun Jul 01 '20 at 16:25
  • Try set your value to handleChange, than debouncedCallApi, then callApi -> state, after this useEffect trigger your function ^_^ – CuteShaun Jul 01 '20 at 16:36
3

That's not so easy question

On one hand to just work around error you are getting, you need to wrap up you setVariables in the function:

 search(e){
  let str = e.target.value;
  _.debounce(() => this.props.relay.setVariables({ query: str }), 500);
}

On another hand, I belive debouncing logic has to be incapsulated inside Relay.

vittore
  • 17,449
  • 6
  • 44
  • 82
3

A lot of the answers here I found to be overly complicated or just inaccurate (i.e. not actually debouncing). Here's a straightforward solution with a check:

const [count, setCount] = useState(0); // simple check debounce is working
const handleChangeWithDebounce = _.debounce(async (e) => {
    if (e.target.value && e.target.value.length > 4) {
        // TODO: make API call here
        setCount(count + 1);
        console.log('the current count:', count)
    }
}, 1000);
<input onChange={handleChangeWithDebounce}></input>
lionbigcat
  • 803
  • 6
  • 13
3

Improving on this answer: https://stackoverflow.com/a/67941248/2390312

Using useCallback and debounce is known to cause an eslint exhaustive deps warning.

Here's how to do it with functional components and useMemo

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

function Input() {
    const [value, setValue] = useState('');

    const debounceFn = useMemo(() => debounce(handleDebounceFn, 1000), []);

    function handleDebounceFn(inputValue) {
        axios.post('/endpoint', {
          value: inputValue,
        }).then((res) => {
          console.log(res.data);
        });
    }


    function handleChange (event) {
        setValue(event.target.value);
        debounceFn(event.target.value);
    };

    return <input value={value} onChange={handleChange} />
}

We are using useMemo to return a memoized value, where this value is the function returned by debounce

kevgathuku
  • 316
  • 5
  • 16
1

Some answers are neglecting that if you want to use something like e.target.value from the event object (e), the original event values will be null when you pass it through your debounce function.

See this error message:

Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property nativeEvent on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().

As the message says, you have to include e.persist() in your event function. For example:

const onScroll={(e) => {
  debounceFn(e);
  e.persist();
}}

Then of course, your debounceFn needs to be scoped outside of the return statement in order to utilize React.useCallback(), which is necessary. My debounceFn looks like this:

const debounceFn = React.useCallback(
  _.debounce((e) => 
      calculatePagination(e), 
      500, {
            trailing: true,
      }
  ),
  []
);
Justin Jaeger
  • 181
  • 1
  • 6
  • 1
    I would avoid using `useCallback` with the debouncer. This will force all `useState` hooks to reset to the initial state from when the page was loaded. – Fiddle Freak Dec 27 '21 at 21:31
0

@Aximili

const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));

looks strange :) I prefare solutions with useCallback:

const [searchFor, setSearchFor] = useState('');

const changeSearchFor = debounce(setSearchFor, 1000);
const handleChange = useCallback(changeSearchFor, []);
0

for your case, it should be:

search = _.debounce((e){
 let str = e.target.value;
 this.props.relay.setVariables({ query: str });
}, 500),
Kaiwen Luo
  • 370
  • 4
  • 10
0
class MyComp extends Component {
  debounceSave;
  constructor(props) {
    super(props);
  }
  this.debounceSave = debounce(this.save.bind(this), 2000, { leading: false, trailing: true });
}

save() is the function to be called

debounceSave() is the function you actually call (multiple times).

Nismi Mohamed
  • 670
  • 1
  • 7
  • 7
0

This worked for me:

handleChange(event) {
  event.persist();
  const handleChangeDebounce = _.debounce((e) => {
    if (e.target.value) {
      // do something
    } 
  }, 1000);
  handleChangeDebounce(event);
}
0

This is the correct FC approach @

Aximili answers triggers only one time

import { SyntheticEvent } from "react"

export type WithOnChange<T = string> = {
    onChange: (value: T) => void
}

export type WithValue<T = string> = {
    value: T
}

//  WithValue & WithOnChange
export type VandC<A = string> = WithValue<A> & WithOnChange<A>

export const inputValue = (e: SyntheticEvent<HTMLElement & { value: string }>): string => (e.target as HTMLElement & { value: string }).value

const MyComponent: FC<VandC<string>> = ({ onChange, value }) => {
    const [reload, setReload] = useState(false)
    const [state, setstate] = useState(value)
    useEffect(() => {
        if (reload) {
            console.log('called api ')
            onChange(state)
            setReload(false)
        }
    }, [reload])

    const callApi = () => {

        setReload(true)
    } // You might be able to call API directly here, I haven't tried
    const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000))

    function handleChange(x:string) {
        setstate(x)
        debouncedCallApi()
    }

    return (<>
        <input
            value={state} onChange={_.flow(inputValue, handleChange)} />
    </>)
}


Chetan Jain
  • 236
  • 6
  • 16
-2
    const delayedHandleChange = debounce(eventData => someApiFunction(eventData), 500);

const handleChange = (e) => {
        let eventData = { id: e.id, target: e.target };
        delayedHandleChange(eventData);
    }
ProblemSolver
  • 636
  • 1
  • 8
  • 16
  • 1
    It is better to explain your answer, what it is doing, how it is solving OP's problem, give some references as required. Just adding a code block or a link is not enough. [Check StackOverflow answer guide](https://stackoverflow.com/help/how-to-answer) – Indigo May 14 '21 at 15:33