Debounced function and useMemo, useCallback

270 views Asked by At

Here is debounce function:

const debounce = (func, delay=1000) => {
    let timeoutId;

    return (...args) => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(()=>func(...args), delay);
    }
}

export default debounce

And here is useHomeOnIdle:

import { useCallback, useEffect, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import debounce from '../js/debounce'

const useHomeOnIdle = (time=5000) => {
    // If mouse is idle for 5 seconds, redirect to home page

    const navigate = useNavigate();
    const debouncedNavigate = debounce(navigate, time);
    //const debouncedNavigate = useCallback(() => debounce(navigate, time), [navigate, time]);
    //const debouncedNavigate = useMemo(()=>debounce(navigate, time), [navigate, time]);
    console.log('debouncedNavigate', debouncedNavigate);

    // useCallback to keep the same function reference for the event listener
    const mouseMoveHandler = useCallback(() => {
        debouncedNavigate('/');
    },[debouncedNavigate]);


    useEffect(() => {
        // Call once to start the timer when the component is mounted
        mouseMoveHandler();

        window.addEventListener('mousemove', mouseMoveHandler);

        return () => window.removeEventListener('mousemove', mouseMoveHandler);
    }, [mouseMoveHandler])
}
export default useHomeOnIdle

If I leave this as is in some case site goes to main page even if I move mouse constantly. This is due to rerenders and not cleaning up properly. And it looks like memoization debouncedNavigate solve the problem. Here is what I tried:

1.

const debouncedNavigate = useCallback(() => debounce(navigate, time), [navigate, time]);

This is not working at all and debouncedNavigate is:

() => debounce(navigate, time)

2.

const debouncedNavigate = useCallback(debounce(navigate, time), [navigate, time]);

This is working but eslint throws:

React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.eslint

debouncedNavigate is:

debouncedNavigate (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(()=>func(...args), delay);
}

3.

const debouncedNavigate = useMemo(()=>debounce(navigate, time), [navigate, time]);

This is working and debouncedNavigate is:

debouncedNavigate (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(()=>func(...args), delay);
}

And there is no eslint warning

4.

const debouncedNavigate = useMemo(debounce(navigate, time), [navigate, time]);

This is not working at all and throws:

Uncaught TypeError: debouncedNavigate is not a function

Question: So it looks like best option is 3. because it's working and no warnings but should I use useMemo to memoize function? And why adding or removing ()=> from useMemo/useCallback make things work or not. And it looks like:

useMemo(()=>debounce(navigate, time), [navigate, time]);

returns same value as:

useCallback(debounce(navigate, time), [navigate, time]);

I'm confused with this useMemo/useCallback usage

2

There are 2 answers

0
Justin Warkentin On BEST ANSWER

When you call debounce during render it returns a new function identity. Consequently, debouncedNavigate is a new function on every render. This also means your mouseMoveHandler changes on every render which means your event binding is for a different function each time. You are on the right track. You just want a memoized debounce function. useMemo takes a function that returns a value and that value is memoized.

const debouncedNavigate = useMemo(() => debounce(navigate, time), [navigate, time]);

Editing to add more explanation:

When you pass a function to useCallback that function is memoized and returned by that call until a dependency changes. When you have an inline function it's great for the linter because it can make sure your dependencies are all correct, but in reality ever render cycle is defining a new function and passing it in only for it to get thrown out if a dependency hasn't changed. It maintains a stable function identity, though. This is why your example in #2 works, even though it gives you a warning. It is creating a new function on every render when you call debounce but they are all getting thrown out except for the first one.

When you call useMemo it receives a function that returns a value to memoize. That inline function identity changes on every render as well. However, it's only called when a dependency changes. So in the case of useMemo it only ever calls debounce one time.

0
Mateo Lee On

Great job on exploring various options to solve this problem! It's a complex scenario, but I'll try to help you achieve a better understanding of the differences between useCallback and useMemo and how they are working in your code.

Firstly, useCallback and useMemo are both hooks provided by React that enable performance optimizations by avoiding unnecessary renders or computations.

  • useCallback is specifically designed to return a memorized version of a function. It's used when you have a potentially expensive operation (like a function that needs to operate on a large array or object), and you only want to rerun it when some of its dependencies change.
  • useMemo, on the other hand, returns a memorized value. Note: Though it can also return function, if a function is what's computed inside, it may confuse the purpose of the two hooks.

Coming back to your code, Option 3 (useMemo) is the most appropriate choice here.

const debouncedNavigate = useMemo(()=>debounce(navigate, time), [navigate, time]);

This code uses useMemo to return a memoized version of debounce(navigate, time). The returned function is stored in debouncedNavigate. This debouncedNavigate function will be the same function across renders until navigate or time changes.

The reason you are seeing similar outputs from useCallback(debounce(navigate, time), [navigate, time]) and useMemo(()=>debounce(navigate, time), [navigate, time]) is because of the nature of your debounce function. In both cases, the debounce function is being called and returns a new function.

useCallback(() => debounce(navigate, time), [navigate, time]) is not equivalent because this version does not invoke debounce until the returned callback is actually called, by which time it's too late for the debouncing behavior.

And useMemo(debounce(navigate, time), [navigate, time]) doesn't work because useMemo expects a function as its first argument but you are passing the result of a function call.

Overall, it's generally better to think of useCallback as being for functions and useMemo as being for values.

Hopefully this clears up some of the confusion you had! The key point to understand here is that useCallback returns a memoized function, whereas useMemo returns a memoized value, which can also be a function if that's what the computation inside returns.