# useDebounceCallback hook explained

# Introduction

In my last [side project](https://github.com/tigerabrodi/jinwooo), I needed to debounce a callback. I ended up writing a custom hook to do so. In this post, I'll explain how it works. The tricky part is grasping why we need the callback ref.

```javascript
import { useCallback, useEffect, useRef } from 'react'

export function useDebounceCallback<Args extends Array<unknown>>(
  callback: (...args: Args) => void,
  delay: number
): (...args: Args) => void {
  // timeoutRef's job is to store the timeout id
  const timeoutRef = useRef<NodeJS.Timeout | null>(null)

  // callbackRef's job is to store the callback function to be called in the timeout
  const callbackRef = useRef(callback)

  useEffect(() => {
    callbackRef.current = callback
  }, [callback])

  const debouncedCallback = useCallback(
    (...args: Args) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current)
      }

      timeoutRef.current = setTimeout(() => {
        callbackRef.current(...args)
      }, delay)
    },
    [delay]
  )

  useEffect(() => {
    // cleanup
    // When unmounting, clear the timeout
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current)
      }
    }
  }, [])

  return debouncedCallback
}
```

# What is a closure?

First, we need to understand what a closure is.

A closure is when a function "remembers" the variables from its outer scope, even when executed later. It captures these variables at the time the function is created. This is a fundamental feature of JavaScript.

Here is an example:

```javascript
function outer() {
  let x = 1;
  return function inner() {
    console.log(x); // Remembers x from outer scope
  };
}

const fn = outer();
fn(); // Prints 1
```

Okay? Why does it matter?

Here is a clearer example why closures are important:

```javascript
let x = 1;
const fn = () => console.log(x); // Captures x=1
x = 2;
setTimeout(fn, 1000); // Will print 1, not 2
```

When calling `fn` later, it will still remember the value of `x` from the outer scope. Even if `x` is changed later.

# Closures and React

Here is a basic React example:

```javascript
function MyComponent() {
  const [count, setCount] = useState(0);

  // This function captures count=0 when component first renders
  const logCount = () => console.log(count);

  // Even if count changes to 1, the timeout will log 0
  setTimeout(logCount, 1000);
}
```

A more interesting example:

```javascript
function MyComponent() {
  const [count, setCount] = useState(0);

  const debouncedLog = useCallback(() => {
    // This creates a closure over count when useCallback runs
    setTimeout(() => {
      console.log(count); // Will always log the count from when useCallback ran
    }, 1000);
  }, []); // Empty deps means callback never updates
}
```

**Here, I hope it's starting to click. If** `count` **was e.g. 5 at the time** `useCallback` **was run, it will always log 5. You can tell** `setTimeout` **to execute an hour later. It's still going to log 5 (even if the value is 1000 by then).**

Why? Because that's what it "remembers".

# The callback ref pattern

```javascript
const debouncedCallback = useCallback(
  (...args: Args) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = setTimeout(() => {
      // What's going on here?
      callbackRef.current(...args);
    }, delay);
  },
  [delay]
);
```

Looking back the previous examples, you might go "this is impossible to update".

Well, it's not. Refs are just mutable objects. In this case, the closure will capture `callbackRef`'s reference.

So when `callbackRef.current` is called, it looks at the REFERENCE of `callbackRef` and THEN gets redirected to the `current` property of `callbackRef`.

The `current` field ALWAYS holds the latest callback.

```javascript
useEffect(() => {
  callbackRef.current = newCallback;
  // In memory: same callbackRef object, but now new `current` field { current: newCallback }
}, [callback]);
```

To be very explicit, setTimeout works as it always does. It creates a closure over `callbackRef`'s reference.

# But Tiger, why is it not closing over `.current` field?

That's a good question.

The key is in how the code is written.

```javascript
setTimeout(() => {
  callbackRef.current(...args); // This is ONE expression
}, delay);

// It's NOT the same as (this would be wrong!):
const currentCallback = callbackRef.current; // Accessing .current NOW
setTimeout(() => {
  currentCallback(...args); // Using captured value (currentCallback)
}, delay);
```

The second bit here GETS the value of `callbackRef.current` before the closure is created. Therefore, a closure will be created over this specific value.

When you write `callbackRef.current(...args)`, you're writing ONE expression that:

1. First gets the reference (callbackRef)
    
2. Then follows that reference
    
3. Then gets the .current property and uses it
    
4. All at execution time!
    

# Recap with final example

```javascript
// Memory model example:
const callbackRef = { current: originalCallback }; // Object in memory
// setTimeout's closure captures this ENTIRE object reference

// Later:
callbackRef.current = newCallback; // SAME object, new value inside
// When timeout runs, follows same reference, gets new value

// VS wrong way (getting value and using it):
const currentCallback = callbackRef.current; // Captures value immediately
// Later changes to callbackRef.current won't affect this
// Because setTimeout closed over this specific value during time of its creation
```

# By Value vs By Reference

If you've a hard time understanding the concept of "reference", I recommend reading this: [By value vs By Reference in JavaScript](https://tigerabrodi.blog/by-value-vs-by-reference-in-javascript).
