useEffect.dev
← Back to lessons

Lesson 9. Debouncing a user input 4. Another debouncing implementation

Let’s see another way of implementing debouncing and why it is necessary in some cases.

Imagine we have a hook useTime at our disposal (we are not allowed to modify it), which returns the current time, and we want to display this time in our component. But the hook returns a new time every 10 milliseconds, while we just want to update the displayed value every 250 milliseconds.

Here is the first version of our component without any debouncing.

Result

As you can see, the displayed time is updated way too fast, and we are just displaying it; imagine if we did some processing at each update.

Can you see why the previous solution for debouncing cannot apply here? It would a little bit like that:

useEffect(() => {
let timeout = setTimeout(() => {
setDebouncedTime(time)
}, 250)
return () => clearTimeout(timeout)
}, [time])

Problem: time is updated every 10 milliseconds, meaning that our timeouts would be systematically cancelled, and our debouncedTime would never be updated.

To solve our problem, we won’t cancel the timeout when time changes. Instead, we will start a new timeout only if the previous one is done. To do that, we need to store the ID of the current timeout.

And since this stored ID must persist from one rendering to another, but we don’t need to trigger a new effect each time is updated, we’ll use a ref.

const timeoutRef = useRef(null)
useEffect(() => {
if (!timeoutRef.current) {
timeoutRef.current = setTimeout(() => {
setDebouncedTime(time)
timeoutRef.current = null
}, 250)
}
}, [time])

Notice that we don’t return a clean-up function to clear the timeout. Indeed we don’t want to cancel the timeout each time time is updated. But still, we need to be sure we don’t update the state calling setDebouncedTime when the component is unmounted.

To make sure of that, we can declare another useEffect without any value in the dependency array. The clean-up function we return will be executed only when the component is unmounted for good, so we can clear any remaining timeout there:

useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])

Here is the full version of our component with debouncing:

Result