Cover Image for Why ChatGPT (+ many tutorials) are wrong about React & debounce

Why ChatGPT (+ many tutorials) are wrong about React & debounce

Bartek Witczak
Bartek Witczak

I needed to implement debounce. I recalled seeing a solution somewhere in our codebase. I’ve used copy’n’paste strategy, adjusted code to work in my use case, run the app and … it wasn’t working 🤔

What’s going on?

That’s how my story with debounce started. I’ve already implemented debounce dozen of times. Now, instead of writing it myself, I’ve copied “working” solution from codebase. But it didn’t make sense. I didn’t even think that current solution wasn’t working, so I started debugging my part…

Quick recap -> What’s debounce?

Debounce is used in several use cases. The most common are:

  • search
  • autocomplete
  • loading options in dropdown The idea is simple. You don’t want to “spam” backend with multiple request when making search. Usually users type few characters or even whole words, then wait for results. You don’t want to make request for each keystroke. That’s when you need debounce. It’s deferred function call with twist. Debounce delays a function call until after a certain period of inactivity. Since image is 1000 words:
Debounce

I guess usually you’d use lodash. If you’re curious, that’s simple implementation. (Lodash implementation provides more options like cancel or flush)

function debounce(
func: (...args: unknown[]) => void,
delay: number
): (...args: unknown[]) => void {
let debounceTimer: ReturnType<typeof setTimeout>

return function (...args: unknown[]) {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => func(...args), delay)
}
}

How to check if debounce is working (correctly 😉)?

The easiest way is, of course, to put console.log everywhere 💃🕺 And if you’re working with network requests, then definitely check out Network tab. If it doesn’t work, you’ll see waterfall.

Chrome Network Tab - Waterfall Effec

Don’t worry. It’s going to be easy to spot if you got debounce working.

ChatGPT & every other tutorial 🙈

That’s the context. I wanted to make things clear. The real question of that blog post is why every tutorial ChatGPT got it wrong? 🤔 If you search for debounce + React, you’ll got something like this:

const Form = () => {
const debouncedSendRequest = debounce(
value => console.log('Send request: ', value),
500
)

const onChange = e => {
debouncedSendRequest(e.target.value)
}

return <input onChange={onChange} />
}

What’s wrong in that case?

There are basically 2 problems with that solution and they are mutually exclusive 😳

First of all, you probably are using controlled components(or at least I hope so). Then you’ll adapt that solution to your use case & you’ll see that … it’s not working. If you

const Form = () => {
const [value, setValue] = useState('')
const debouncedSendRequest = debounce(
value => console.log('Send request: ', value),
500
)

const onChange = e => {
setValue(e.target.value)
debouncedSendRequest(e.target.value)
}

return <input value={value} onChange={onChange} />
}

The second problem is that you’re using uncontrolled components. If you’re aware of the difference between controlled vs. uncontrolled components, that’s great. Otherwise, you’re doomed. There will be the day. I’d argue that React main superpower is determinist state, also UI state. So playing with uncontrolled components is dangerous game. Throughout 9 years of working with React, uncontrolled components were the most common bugs I’ve seen. (That could also be the case that you use uncontrolled component & live happily 😬 Just understand the consequences.)

How to do it right?

const Form = () => {
const [value, setValue] = useState('')

const debouncedSendRequest = useMemo(() => {
return debounce(value => console.log('Sending request:', value), 1000)
}, [])

const onChange = e => {
const value = e.target.value
setValue(value)
debouncedSendRequest(value)
}

return <input onChange={onChange} value={value} />
}

The key to correct implementation is useMemo. Due to React re-renders, every time the state changes, new function is created. We need to wrap our debounced function in useMemo so that we’re reusing the same function. In that simple case, we’re using arrow function. We define “sendRequest” function on the fly. Often we’ll have already defined function.

const sendRequest = value => console.log('Sending request:', value)

We’ll need to wrap it into useCallback 🙈 since we need to put sendRequest to dependency list of useMemo.

const sendRequest = useCallback(
value => console.log('Sending request:', value),
[]
)

const debouncedSendRequest = useMemo(() => {
return debounce(sendRequest, 1000)
}, [sendRequest])

(They say it’s going to be simpler when “new React compiler” arrives 😎)

Is it harder to be junior dev in ChatGPT era?

Definitely, you can find tutorials about debounce & React that present correct solution. Still I’ve found most tutorials to be incorrect about debounce. Since LLM is trained on that enormous content, it’s quite obvious that’s going to provide wrong solution 😒

For me, LLMs & Copilots are great solutions. It’s increasing my productivity. It makes writing code an addition to solving problems. I’m just curious how that’s going to impact broader ecosystem. I’m really curious what’s the impact on junior devs. First-order consequence seems to be positive. Developers are more productive. Junior dev can implement more stuff with basic knowledge. And what about second-order consequences?