45

it would be best to first look at my code:

import React, { Component } from 'react';
import _ from 'lodash';
import Services from 'Services'; // Webservice calls

export default class componentName extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: this.props.value || null
    }
  }

  onChange(value) {
    this.setState({ value });

    // This doesn't call Services.setValue at all
    _.debounce(() => Services.setValue(value), 1000);
  }

  render() {
    return (
      <div>
        <input 
          onChange={(event, value) => this.onChange(value)}
          value={this.state.value}
        />
      </div>
    )
  }
}

Just a simple input. In the contructor it grabs value from the props (if available) at sets a local state for the component.

Then in the onChange function of the input I update the state and then try to call the webservice endpoint to set the new value with Services.setValue().

What does work is if I set the debounce directly by the onChange of the input like so:

<input 
  value={this.state.value} 
  onChange={_.debounce((event, value) => this.onChange(value), 1000)} 
/>

But then this.setState gets called only every 1000 milliseconds and update the view. So typing in a textfield ends up looking weird since what you typed only shows a second later.

What do I do in a situation like this?

5 Answers5

56

The problem occurs because you aren't calling the debounce function, you could do in the following manner

export default class componentName extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: this.props.value || null
    }
    this.servicesValue = _.debounce(this.servicesValue, 1000);
  }

  onChange(value) {
    this.setState({ value });
    this.servicesValue(value);
  }
  servicesValue = (value) => {
      Services.setValue(value)
  }
  render() {
    return (
      <div>
        <input 
          onChange={(event, value) => this.onChange(value)}
          value={this.state.value}
        />
      </div>
    )
  }
}
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • I see! I could also just wrap _.debounce(...) in a self executing anonymous function in `onChange` correct? –  Dec 14 '17 at 09:26
  • 4
    yes, you could do that too, however, in that case you would be returning a new debounced function everytime, So the above approach is better than what you suggest – Shubham Khatri Dec 14 '17 at 09:27
44

Solution for those who came here because throttle / debounce doesn't work with FunctionComponent - you need to store debounced function via useRef():

export const ComponentName = (value = null) => {
  const [inputValue, setInputValue] = useState(value);

  const setServicesValue = value => Services.setValue(value);

  const setServicesValueDebounced = useRef(_.debounce(setServicesValue, 1000));

  const handleChange = ({ currentTarget: { value } }) => {
    setInputValue(value);
    setServicesValueDebounced.current(value);
  };

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

This medium article perfectly explains what happens:

Local variables inside a function expires after every call. Every time the component is re-evaluated, the local variables gets initialized again. Throttle and debounce works using window.setTimeout() behind the scenes. Every time the function component is evaluated, you are registering a fresh setTimeout callback. So we will use useRef() hook as value returned by useRef() does not get re-evaluated every time the functional component is executed. The only inconvenience is that you have to access your stored value via the .current property.

I've created sandbox with tiny lodash.throttle and lodash.debounce packages so you can experiment with both and choose suitable behavior

godblessstrawberry
  • 4,556
  • 2
  • 40
  • 58
  • 2
    Thank you for this answer. I've got a functional component with a controlled input that updates the state and also sets a value via a service, and your sandbox really nails it. That said, I find useCallback to be an even better fit, as indicated in an update to that Medium article. – Kevin Ashworth Dec 05 '20 at 21:59
15

For a React functional component, debounce does not work by default. You will have to do the following for it to work:

const debouncedFunction= React.useCallback(debounce(functionToCall, 400), []);

useCallback makes use of the function returned by debounce and works as expected. Although, this is a bit more complicated when you want to use state variables inside the debounced function (Which is usually the case).

React.useCallback(debounce(fn, timeInMs), [])

The second argument for React.useCallback is for dependencies. If you would like to use a state or prop variable in the debounced function, by default, it uses an an old version of the state variable which will cause your function to use the historical value of the variable which is not what you need. To solve this issue, you will have to include the state variable like you do in React.useEffect like this:

React.useCallback(debounce(fn, timeInMs), [stateVariable1, stateVariable2])

This implementation might solve your purpose. But you will notice that the debounced function is called every time the state variables (stateVariable1, stateVariable2) passed as dependencies change. Which might not be what you need especially if using a controlled component like an input field.

The best solution I realized is to put some time to change the functional component to a class based component and use the following implementation:

constructor(props)
    {
        super();
        this.state = {...};
        this.functionToCall= debounce(this.functionToCall.bind(this), 400, {'leading': true});
    }
SummmerFort
  • 369
  • 3
  • 8
1

I wrote a hook for those who are using react functional components.

It's typescript, but you can ignore type annotations to use on your javascript application.

| use-debounce.ts |

import { debounce, DebounceSettings } from 'lodash'
import { useRef } from 'react'

interface DebouncedArgs<T> {
  delay?: number
  callback?: (value: T) => void
  debounceSettings?: DebounceSettings
}



export const useDebounce = <T = unknown>({ callback, debounceSettings, delay = 700 }: DebouncedArgs<T>) => {
  const dispatchValue = (value: T) => callback?.(value)

  const setValueDebounced = useRef(debounce(dispatchValue, delay, { ...debounceSettings, maxWait: debounceSettings?.maxWait || 1400 }))

  return (value: T) => setValueDebounced.current(value)
}

| usage: |

export const MyInput: FC = () => {
  const [value, setValue] = useState<string>('')
  const debounce = useDebounce({ callback: onChange })

  const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
    const { value } = evt.currentTarget
    setValue(value)
    debounce(value)
  }

  function onChange(value: string) {
    // send request to the server for example
    console.log(value)
  }

  return <input value={value} onInput={handleOnInput} />
}

qafoori
  • 781
  • 8
  • 13
  • Tip for those needing to set a state from the callback, pass the set function as an argument. Clean and simple, thanks! – CyberEd Feb 06 '23 at 06:14
1

Solution for functional component - use useCallback

export const ComponentName = (value = null) => {
  const [inputValue, setInputValue] = useState(value);

  const setServicesValue = value => Services.setValue(value);

  const setServicesValueDebounced = useCallback(_.debounce(setServicesValue, 500), []);

  const handleChange = ({ currentTarget: { value } }) => {
    setInputValue(value);
    setServicesValueDebounced(value);
  };

  return <input onChange={handleChange} value={inputValue} />;
};
Ananth
  • 77
  • 9