react-timer-hook icon indicating copy to clipboard operation
react-timer-hook copied to clipboard

Memoized callbacks from hooks

Open dartess opened this issue 4 years ago • 3 comments

Hi!

Functions that return from the hook must be memoized because they can be used in effects, and the linter will require you to specify them depending. Now I had to memoize the functions myself, so as not to disable the linter. But after updating the linter, I still had to turn it off. Time to raise the problem.

Simple example (broken code):

    const {
        seconds,
        minutes,
        pause,
        restart,
    } = useTimer({
        expiryTimestamp: Date.now() + RE_SEND_AFTER_SECONDS * 1000,
        onExpire: () => setIsCanReSendSms(true),
    });

    useEffect(
        () => {
            if (screen === 'phoneConfirm') {
                restart(Date.now() + (RE_SEND_AFTER_SECONDS * 1000));
            } else {
                pause();
            }
        },
        [screen, pauseMemoized, restartMemoized],
    );

This will restart the effect with each renderer, because the functions are not memoized. Correct behavior - restarting the effect only when the variable "screen" is changed.

This can be added by removing callbacks from the dependencies. But this will force the developer to disable the linter, which is a bad decision in 99.9%.

Another solution is to add memoization yourself, but this is impractical and is now the reason for a new linter error. Now it looks like this.

    const {
        seconds,
        minutes,
        pause,
        restart,
    } = useTimer({
        expiryTimestamp: Date.now() + RE_SEND_AFTER_SECONDS * 1000,
        onExpire: () => setIsCanReSendSms(true),
    });

    // eslint-disable-next-line react-hooks/exhaustive-deps
    const pauseMemoized = useCallback(pause, []);
    // eslint-disable-next-line react-hooks/exhaustive-deps
    const restartMemoized = useCallback(restart, []);

    useEffect(
        () => {
            if (screen === 'phoneConfirm') {
                restartMemoized(Date.now() + (RE_SEND_AFTER_SECONDS * 1000));
            } else {
                pauseMemoized();
            }
        },
        [screen, pauseMemoized, restartMemoized],
    );

I think the problem needs to be solved radically. That is, in the source.

I was solving a similar problem nearby: https://github.com/pbeshai/use-query-params/pull/46

dartess avatar May 21 '20 13:05 dartess

Running into the same problem. Specifying a timer callback as a dependency infinitely triggers useEffect if any state is set inside the useEffect.

  const {
    seconds,
    minutes,
    pause,
    resume,
    restart
  } = useTimer({
    expiryTimestamp: convertDurationToExpiryTimestamp(durationInSeconds ? durationInSeconds : 60),
    onExpire: () => {}});

useEffect(() => {
  console.log("rerunning");
  if(activityState === "someValue") {
    restart();
    pause();
    setActivityState(newState);
  }
}, [activityState, pause, restart]);

In this example, the pause/restart callback changes with every render, which triggers the useEffect, which causes another render (via setActivityState), which changes the pause/restart callback, which calls the useEffect, etc...

dylanmrowe avatar Dec 30 '20 20:12 dylanmrowe

Should be solved (for useTimer at least) by https://github.com/amrlabib/react-timer-hook/pull/44

dylanmrowe avatar Dec 30 '20 23:12 dylanmrowe

@amrlabib could you please look into this?

chybisov avatar May 02 '22 10:05 chybisov

Done in v3.0.6

amrlabib avatar May 13 '23 23:05 amrlabib