Debouncing React controlled components
Tags: react, javascript, frontend, tutorial
Published: 2021-09-27 14:11:46

- [Motivation](#motivation)
- [What is debouncing?](#what-is-debouncing)
- [Controlled and Uncontrolled components](#controlled-and-uncontrolled-components)
  - [Controlled](#controlled)
  - [Uncontrolled](#uncontrolled)
- [Debouncing Controlled components](#debouncing-controlled-components)
- [`useDebounce` hook](#usedebounce-hook)
- [Real-life use cases](#real-life-use-cases)

## Motivation

In my most recent application, I came across the need to debounce some form fields. Every time I had to debounce, it's usually an uncontrolled component. This time, I had to debounce a controlled component. A normal debounce function wouldn't work as expected, so I had to use another method and ended up creating a `useDebounce` hook for reusability.

## What is debouncing?

If you don't know what it is, `debounce` is usually a set of code that keeps a function from running too many times. You can read more about it in [this article](https://www.freecodecamp.org/news/javascript-debounce-example).
It is usually used for user actions to prevent the user from spamming too many requests to the server. A usual use case is in search or toggle inputs. We listen to the user inputs and only send the result to the server when no more inputs coming in.
Let's see some example

```js
/**
 * A basic debounce function.
 * Most implementations you'll see look like this.
 * @params {VoidFunction} callback - A callback function to be called after timeout ends
 * @params {number} timeout - Timeout in milliseconds
 * @returns {VoidFunction} - A function to execute the callback
 */

function debounce(callback, timeout = 500) {
  let timer

  // inner function
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => callback.apply(this, args), timeout)
  }
}
```

> The debounce function sets a timer(500ms in our example), when the inner function is called before the timer ends, we cancel the timer and start over. The callback function is only triggered when the timer ends without being interrupted.

See a detailed implementation on [codesandbox](https://codesandbox.io/s/debouncing-uncontrolled-form-component-0w5wy?file=/src/utils.js:0-554)

using in our component;

```jsx
<input
  name="search"
  type="search"
  id="search-input"
  onChange={debounce(handleChange)}
/>
```

See a detailed implementation on [codesandbox](https://codesandbox.io/s/debouncing-uncontrolled-form-component-0w5wy?file=/src/App.js:1855-2201)

This is an example with an [uncontrolled component](#uncontrolled)

## Controlled and Uncontrolled components

### [Controlled](https://reactjs.org/docs/forms.html#controlled-components)

In a React controlled component, the input value is set by the `state`. The `onChange` handler listens to input changes and stores the value into the state. The input value is then updated with the value stored in the state.

```jsx
function Controlled() {
  const [value, setValue] = useState()

  const handleChange = event => {
    setValue(event.target.value)
  }

  const handleSubmit = event => {
    event.preventDefault()
    console.log({ value })
  }

  return (
    <form id="search" onSubmit={handleSubmit}>
      <label htmlFor="search-input">Search</label>
      <input
        id="search-input"
        name="search"
        type="search"
        value={value}
        onChange={handleChange}
      />
      <button type="submit">Search</button>
    </form>
  )
}
```

Edit on [codesandbox](https://codesandbox.io/s/trusting-proskuriakova-jw3x5?file=/src/App.js:66-623)

### [Uncontrolled](https://reactjs.org/docs/uncontrolled-components.html)

In an uncontrolled component, instead of updating the values with the state, you can use [a ref](https://reactjs.org/docs/refs-and-the-dom.html) to get form values from the DOM. Basically, in an uncontrolled component, we allow the form elements to update their values with the normal HTML form behaviour
For example

```jsx
function UnControlled() {
  const inputRef = useRef(null)

  const handleSubmit = event => {
    event.preventDefault()
    console.log({ value: inputRef.current.value })
  }

  return (
    <form id="search" onSubmit={handleSubmit}>
      <label htmlFor="search-input">Search</label>
      <input ref={inputRef} id="search-input" name="search" type="search" />
      <button type="submit">Search</button>
    </form>
  )
}
```

> The input field is updated by the DOM. We select the input element with our `inputRef` and then read the value when we need it.

Edit on [codesandbox](https://codesandbox.io/s/trusting-proskuriakova-jw3x5?file=/src/App.js:625-1056)

## Debouncing Controlled components

We've already seen how to debounce an uncontrolled component [in our first example](#what-is-debouncing). You can also see and interact with the example on [codesandbox](https://codesandbox.io/s/debouncing-uncontrolled-form-component-0w5wy?file=/src/App.js).

The approach used in the example doesn't work for controlled components.
Instead of writing a debounce function to debounce our input,

```jsx
function Controlled() {
  const timerRef = useRef(null) // Store the previous timeout
  const [value, setValue] = useState()
  const [user, setUser] = useState()

  const fetchUserDetails = useCallback(async () => {
    try {
      const [userDetails] = await fetch(`${API}?name=${value}`).then(res =>
        res.json()
      )

      setUserDetails(prevDetails => ({ ...prevDetails, ...userDetails }))
    } catch (error) {
      console.log(error)
    }
  }, [value])

  // Producing the same behaviour as the 'inner function' from the debounce function
  useEffect(() => {
    clearTimeout(timerRef.current) // clear previous timeout

    timerRef.current = setTimeout(() => {
      timerRef.current = null // Reset timerRef when timer finally ends
      fetchUserDetails()
    }, 500)

    return () => clearTimeout(timerRef.current)
  }, [fetchUserDetails])

  const handleChange = event => {
    setValue(event.target.value)
    console.log(event.target.value)
  }

  return (
    <form id="search">
      <label id="search-label" htmlFor="search-input">
        Search for user details
      </label>
      <input
        name="search"
        type="search"
        id="search-input"
        value={value}
        onChange={handleChange}
      />
    </form>
  )
}
```

> Instead of storing the previous timer in a [lexical scope](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#lexical_scoping), we store it in [a ref](https://reactjs.org/docs/refs-and-the-dom.html) and then send our request to the server with the `useEffect` hook.
> It's a simple implementation but we have one problem. It's not reusable. We need to create a custom hook for this.

## `useDebounce` hook

```js
import { useEffect, useRef } from "react"

/**
 * @callback callbackFunc
 * @param {any[]} args - arguments passed into callback
 */
/**
 * Debounce function to reduce number executions
 * @param {callbackFunc} cb - callback function to be executed
 * @param {number} wait - number of milliseconds to delay function execution
 * @param {any[]} deps - dependencies array
 */
const useDebounce = (cb, wait = 500, deps = []) => {
  const timerRef = useRef(null)

  useEffect(() => {
    clearTimeout(timerRef.current)

    timerRef.current = setTimeout(() => {
      cb.apply(this, args)
    }, wait)

    return () => clearTimeout(timerRef.current)
    /** used JSON.stringify(deps) instead of just deps
      * because passing an array as a dependency causes useEffect 
re-render infinitely
      * @see {@link https://github.com/facebook/react/issues/14324}
      */
    /* eslint-disable react-hooks/exhaustive-deps */
  }, [cb, wait, JSON.stringify(deps)])
}
```

> _My implementation isn't perfect and may contain bugs but it works fine for my case. Feel free to improve it and share yours in the comments._

Now we can `useDebounce` in our component;

```jsx
function Controlled() {
  const [value, setValue] = useState()
  const [user, setUser] = useState()

  // Debounce our search
  useDebounce(async () => {
    try {
      const [userDetails] = await fetch(`${API}?name=${value}`)
                                                             .then(res => res.json())

      setUserDetails(prevDetails => ({ ...prevDetails, ...userDetails }))
    } catch (error) {
      console.log(error)
    }
  }, 500, [value])

  const handleChange = event => {
    setValue(event.target.value)
    console.log(event.target.value)
  }

  return (
    <form id="search">
      <label id="search-label" htmlFor="search-input">
        Search for user details
      </label>
      <input
        name="search"
        type="search"
        id="search-input"
        value={value}
        onChange={handleChange}
      />
    </form>
  )
}
```

See detailed implementation on [codesandbox](https://codesandbox.io/s/debouncing-controlled-form-fields-1mb86?file=/src/App.js:510-1277)

## Real-life use cases

I'm currently working on an [app](http://pizza-app-mauve.vercel.app).
In my app, for each item in the cart, the user can add different sizes and also increment or decrement the quantities of each size.
The sizes and quantities are parsed into an object and stored in context before being sent to the server.
![Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yh9eeeubgqippzus4q05.gif)

While exploring this topic, I created a [demo application](https://codesandbox.io/s/jovial-engelbart-tu6jk?file=/src/App.js) for validating a sign-up form with an API in real-time.

_After writing this article, I found a [different approach on usehooks.com](https://usehooks.com/useDebounce) to this and I recommend checking it out_