DEV Community

Ramesh Vishnoi
Ramesh Vishnoi

Posted on • Updated on

Debouncing Simplified

Another frequently asked question in interviews these days is Debouncing. Debouncing is important concept used in Javascript to improve performance, prevent unnecessary wastage of resource.

Debouncing is a mechanism in which a frequently called function is called once after some threshold time limit is reached.

Lets take example - you have a search input box in UI, now you want to display result on every keyinput by user (similar to how google display search result).

But fetching result from api on every input charater is expensive - there will be network calls, computation on backend and ui update multiple times.

If a user want to search for baking receipe then it comes to total 14 characters l.e 14 api call will be made to backend, but out of 14, only 1 or 2 are enough to display relevant result.

so on an average you make 12 api call which are not required, and if your application have 1000s of user then you multiply wasted api calls accordingly. Because resource wastage is on backend side, so 12 * 1000 api calls, this is only one search query. If your average user is making 10 search query on a day = 12 * 1000 * 10. You can see where i am going with this.

We can avoid making unnecessary api calls by using debouncing. We design frontend UI in a way that on every character input, system wait for some time (it can be 1 second or 500 millisecond depending on your application), and during this time if there is another input then we wait again and only make api call (with complete user input) if there is no further input.

Implementation -

Consider a React application

const HomeScreen = () => {
    const [searchQuery, setSearchQuery] = useState("")

    const triggerSearch = (e) => {
        setSearchQuery(e.target.value)
        fetchResult(e.target.value)
    }

    const fetchResult = () => {
        console.log("API called for following input :: ", input)
    }

    return (
        <div className="App">
            <label>Search for: </label>
            <input value={searchQuery} onInput={triggerSearch}/>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

If we type for baking receipe the fetchResult function get called 14 times
Bad code example

Solution :

const HomeScreen = () => {
    const [searchQuery, setSearchQuery] = useState("")
    const timer = useRef();

    const triggerSearch = (e) => {
        setSearchQuery(e.target.value)
        debouncedFetchResult(e.target.value)
    }

    const fetchResult = (input) => {
        console.log("API called for following input :: ", input)
    }

    const debouncedFetchResult = (input) => {
        if (timer.current) {
            // if there was any pending api call then cancel it and request new
            clearTimeout(timer.current)
        }

        // here we are waiting for new input for 1second. you can change accordingly
        let timeoutInstance = setTimeout(() => {
            fetchResult(input)
        },1000)

        timer.current = timeoutInstance;
    }

    return (
        <div className="App">
            <label>Search for: </label>
            <input value={searchQuery} onInput={triggerSearch}/>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Here, on every input character, we call setTimeout and store its reference inside state variable. And if there is any old reference available then we cancel old setTimeout and create a new only.

Output -
Optimized Code output

Top comments (5)

Collapse
 
brense profile image
Rense Bakker

Don't store the timeout in a state variable though. Better to use a ref. There's no need to rerender the component each time the debounce is triggered, which is what happens if you use a state variable.

Collapse
 
rv90904 profile image
Ramesh Vishnoi

@brense - Good point. Don't know how i missed it. Will update code soon.

Thanks

Collapse
 
insidewhy profile image
insidewhy

You should add javascript after the three backticks to get syntax highlighting.

But also there's a lot of issues with this debounce code. I'd refactor it into a single hook via useEffect, or just use a third party library until you learn react better.

Collapse
 
rv90904 profile image
Ramesh Vishnoi

Thanks for the formatting tip.
Idea is to explain it in simple approach or enable young engineers to write their version from scratch instead of relying on any 3rd party library.
Regarding implementation via useEffect - that's another approach, better than this one also. Will share it in comments as well.

Collapse
 
rv90904 profile image
Ramesh Vishnoi • Edited
const HomeScreen = () => {
    const [searchQuery, setSearchQuery] = useState("")

    useEffect(() => {
        let timeoutInstance;
        if (searchQuery) {
            timeoutInstance = setTimeout(() => {
                fetchResult(searchQuery)
            },1000)
        }

        return () => {
            if (timeoutInstance) {
                clearTimeout(timeoutInstance)
            }
        }
    },[searchQuery])

    const triggerSearch = (e) => {
        setSearchQuery(e.target.value)
    }

    const fetchResult = (input) => {
        console.log("API called for following input :: ", input)
    }

    return (
        <div className="App">
            <label>Search for: </label>
            <input value={searchQuery} onInput={triggerSearch}/>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Here, we are relying on useEffect's cleanup function to clear any pending api call.