Michael Kaufman
Michael Kaufman

Reputation: 803

Lodash debounce with React Input

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);
},

Upvotes: 58

Views: 175229

Answers (16)

Jake Hall
Jake Hall

Reputation: 2102

If your react architecture is complex, it can be hard to avoid capturing state using lodash's debounce function. It can ultimately be simpler to do a pure react version of debounce. The code below generates a new function every time the search state is changed, so the state captured is current, but manages the debounce via storing the call in a timer and cancelling via that ref.

  const [lastSearch, setLastSearch] = useState('');
  const [search, setSearch] = useState('');
  const searchTimerRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    // avoid firing this effect initially
    if (lastSearch === search) {
      return;
    }
    setLastSearch(search);
    // this cancels the previous call if it exists
    if (searchTimerRef.current) {
      clearTimeout(searchTimerRef.current);
    }
    searchTimerRef.current = setTimeout(() => {
      callApi();
    }, 750);
  }, [search]);

  const handleSearchChange = (search: string) => {
    setSearch(search);
  };

Upvotes: 1

Jules Patry
Jules Patry

Reputation: 1029

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} />
}

Upvotes: 92

Evaldas
Evaldas

Reputation: 206

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.

Upvotes: 0

Zoha
Zoha

Reputation: 586

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), []);

Upvotes: 2

kevgathuku
kevgathuku

Reputation: 348

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

Upvotes: 6

Justin Jaeger
Justin Jaeger

Reputation: 321

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,
      }
  ),
  []
);

Upvotes: 1

Chetan Jain
Chetan Jain

Reputation: 241

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)} />
    </>)
}


Upvotes: 0

ProblemSolver
ProblemSolver

Reputation: 644

    const delayedHandleChange = debounce(eventData => someApiFunction(eventData), 500);

const handleChange = (e) => {
        let eventData = { id: e.id, target: e.target };
        delayedHandleChange(eventData);
    }

Upvotes: -2

lionbigcat
lionbigcat

Reputation: 993

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>

Upvotes: 3

Faisal siddiqui
Faisal siddiqui

Reputation: 9

This worked for me:

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

Upvotes: 0

Nismi Mohamed
Nismi Mohamed

Reputation: 762

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).

Upvotes: 0

Kaiwen Luo
Kaiwen Luo

Reputation: 513

for your case, it should be:

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

Upvotes: 0

muzykolog84
muzykolog84

Reputation: 11

@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, []);

Upvotes: 0

Aximili
Aximili

Reputation: 29464

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} />
  </>);
}

Upvotes: 22

Jeff Wooden
Jeff Wooden

Reputation: 5479

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)
}

Upvotes: 43

vittore
vittore

Reputation: 17579

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.

Upvotes: 3

Related Questions