Lodash debounce with React Input
Asked Answered
R

15

54

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);
},
Refrigerator answered 29/3, 2016 at 20:6 Comment(1)
try this freecodecamp.org/news/debounce-and-throttle-in-react-with-hooksSportswoman
R
43

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)
}
Rowan answered 29/3, 2016 at 21:43 Comment(6)
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.Refrigerator
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!!Refrigerator
@Jeff Wooden fidden is brokenPyxis
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.Causeuse
@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).Rowan
it would be nice if we could see a solution using react hooks.Nonrecognition
A
78

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

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

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

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

    return <input value={value} onChange={handleChange} />
}
Artery answered 11/6, 2021 at 17:29 Comment(6)
Thank you jules!Berliner
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.Nonrecognition
@FiddleFreak I have never experienced that. I think the issue is something else.Artery
@JulesPatry I actually got around that problem by doing this instead > #70501916Nonrecognition
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.Collincolline
I get React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead. eslintreact-hooks/exhaustive-deps github.com/facebook/react/issues/14920Dual
R
43

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)
}
Rowan answered 29/3, 2016 at 21:43 Comment(6)
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.Refrigerator
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!!Refrigerator
@Jeff Wooden fidden is brokenPyxis
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.Causeuse
@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).Rowan
it would be nice if we could see a solution using react hooks.Nonrecognition
N
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} />
  </>);
}
Neuro answered 5/8, 2019 at 23:21 Comment(2)
useEffect trigger only one time, because reload after first call will be always true.Fenner
Try set your value to handleChange, than debouncedCallApi, then callApi -> state, after this useEffect trigger your function ^_^Fenner
U
4

Improving on this answer: https://mcmap.net/q/334907/-lodash-debounce-with-react-input

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

Ullyot answered 18/5, 2022 at 10:24 Comment(1)
I get React Hook useMemo received a function whose dependencies are unknown. Pass an inline function instead. eslintreact-hooks/exhaustive-depsDual
B
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.

Braid answered 29/3, 2016 at 20:14 Comment(0)
T
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>
Telephotography answered 16/3, 2021 at 7:20 Comment(1)
this won't work at all inside a functional component. You're re-creating the call on every render.Prude
G
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,
      }
  ),
  []
);
Gilder answered 7/9, 2021 at 21:10 Comment(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.Nonrecognition
B
1

if you have

React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead. eslintreact-hooks/exhaustive-deps

warning just use the useMemo instead

const debounceFunc = useMemo(() => _debounce(func, 500), []);
Brusa answered 28/1 at 1:55 Comment(0)
S
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, []);
Saboteur answered 26/1, 2020 at 14:27 Comment(0)
A
0

for your case, it should be:

search = _.debounce((e){
 let str = e.target.value;
 this.props.relay.setVariables({ query: str });
}, 500),
Arlyn answered 17/5, 2020 at 22:42 Comment(0)
A
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).

Amandaamandi answered 3/11, 2020 at 7:2 Comment(0)
L
0

This worked for me:

handleChange(event) {
  event.persist();
  const handleChangeDebounce = _.debounce((e) => {
    if (e.target.value) {
      // do something
    } 
  }, 1000);
  handleChangeDebounce(event);
}
Lamellibranch answered 12/1, 2021 at 9:57 Comment(1)
i think it calls n no of times , and just applies a delay as such, times , i tried it now at least.Sagunto
E
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)} />
    </>)
}


Emitter answered 14/6, 2021 at 8:14 Comment(0)
N
0

debounce utility function

import { useEffect, useRef, useState } from 'react';

export function useDebounce(value, debounceTimeout) {
  const mounted = useRef(false);
  const [state, setState] = useState(value);

  useEffect(() => {
    if (!mounted.current) {
      mounted.current = true;
      return;
    }

    const handler = setTimeout(() => setState(value), debounceTimeout);
    return () => clearTimeout(handler);
  }, [value]);

  return state;
}

use it like this

  const [titleFilterState, setTitleFilterState] = React?.useState('');

const debounceSearchValue = useDebounce(titleFilterState, 300);

then you can use debounceSearchValue as your title value its just it is going to be delayed to not make data fetch after each key press.

Natatory answered 9/3 at 9:43 Comment(0)
S
-2
    const delayedHandleChange = debounce(eventData => someApiFunction(eventData), 500);

const handleChange = (e) => {
        let eventData = { id: e.id, target: e.target };
        delayedHandleChange(eventData);
    }
Sagunto answered 13/5, 2021 at 18:0 Comment(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 guideBee

© 2022 - 2024 — McMap. All rights reserved.