Confused about the strange performance of useMemo Hook in React

476 views Asked by At

This is my own hook which is used to realize debounce in React Hooks

import { useMemo, useRef } from "react";
import { debounce } from "lodash";

export function useDebounceFn(fn, wait) {
  const fnRef = useRef(fn)
  fnRef.current = () => {
    debugger
    fn()
  }

  const debounceFn = useMemo(() => {
    // Case One: can get the newest fn everytime
    return debounce(() => {
      fnRef.current()
    });

    // Case Two: only can get the first fn
    return debounce(fnRef.current);
  }, []);

  return {
    run: debounceFn,
    cancel: debounceFn.cancel,
  }
}

I think debounce(fnRef.current) is equal to debounce(() => { fnRef.current() }) ,but the truth is that they are different and i wonder why. Is there any wrong with my code or just useMemo do something different in these two cases.

  const { run } = useDebounceFn(() => {
    console.log("num", num);
  });

  useEffect(() => {
    run();
  }, [num]);

When i use case one, i can get the newest num and this is what i want to realize, but when i use case two, everytime run function only can get the inital num. i want to know why the case two only can get the inital value, in others word, why the case one can always get the newest value in useMemo.

1

There are 1 answers

0
Drew Reese On

I think debounce(fnRef.current) is equal to debounce(() => { fnRef.current() }), but the truth is that they are different and I wonder why.

  • Case 1 returns a memoized function that calls a function that gets the current ref value and invokes it. When the returned run function is invoked, the current ref value is "unpacked".
  • Case 2 closes over the initial ref value in the decorated debounced function. It will never be updated for the life of the component.

You could fix case 2 by populating the useMemo hook's dependency array to re-enclose the current ref value.

Example:

export function useDebounceFn(fn, wait) {
  const fnRef = useRef(fn);

  const debounceFn = useMemo(() => {
    // Re-"cache" the function
    fnRef.current = () => {
      debugger
      fn()
    }

    // Re-decorate with debounce HOF
    return debounce(fnRef.current);
  }, [fn]);

  return {
    run: debounceFn,
    cancel: debounceFn.cancel,
  }
}

BTW, for case 1 you should use an useEffect hook to set the ref value when the fn updates.

Example:

export function useDebounceFn(fn, wait) {
  const fnRef = useRef(fn);

  useEffect(() => {
    fnRef.current = () => {
      debugger
      fn();
    }
  }, [fn]);

  const debounceFn = useMemo(() => {
    return debounce(() => {
      fnRef.current();
    });
  }, []);

  return {
    run: debounceFn,
    cancel: debounceFn.cancel,
  }
}