useDebounceCallback hook explained
Timeouts, Closures, JavaScript and references.

Just a guy who loves to write code and watch anime.
Search for a command to run...
Timeouts, Closures, JavaScript and references.

Just a guy who loves to write code and watch anime.
let x = 1; const fn = () => console.log(x); // Captures x=1 x = 2; setTimeout(fn, 1000); // Will print 1, not 2
this will print 2 not 1 i have tried it any explaination why?
Qualities that make outstanding builders.

Bipedal vs quadrupedal: how many legs This is about the number of legs the creature walks on. Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids. Quadrupedal. Four legs. Do

Intro You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another. The problem

What is Kinematics Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver

Intro Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more. This post is about the less obvious

In my last side project, 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.
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
}
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:
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:
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.
Here is a basic React example:
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:
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".
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.
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.
.current field?That's a good question.
The key is in how the code is written.
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:
First gets the reference (callbackRef)
Then follows that reference
Then gets the .current property and uses it
All at execution time!
// 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
If you've a hard time understanding the concept of "reference", I recommend reading this: By value vs By Reference in JavaScript.