Skip to navigationSkip to content

Debouncing React controlled components



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 hook for reusability.

What is debouncing?

If you don't know what it is, is usually a set of code that keeps a function from running too many times. You can read more about it in this article. 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

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

using in our component;

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

See a detailed implementation on codesandbox

This is an example with an uncontrolled component

Controlled and Uncontrolled components


In a React controlled component, the input value is set by the . The 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.

function Controlled() { const [value, setValue] = useState() const handleChange = event => { setValue( } 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


In an uncontrolled component, instead of updating the values with the state, you can use a ref 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

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 and then read the value when we need it.

Edit on codesandbox

Debouncing Controlled components

We've already seen how to debounce an uncontrolled component in our first example. You can also see and interact with the example on codesandbox.

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

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( console.log( } 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, we store it in a ref and then send our request to the server with the hook. It's a simple implementation but we have one problem. It's not reusable. We need to create a custom hook for this.


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} */ /* 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 in our component;

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( console.log( } 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

Real-life use cases

I'm currently working on an 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.


While exploring this topic, I created a demo application for validating a sign-up form with an API in real-time.

After writing this article, I found a different approach on to this and I recommend checking it out