reactjsreact-nativereact-hookssettimeoutcode-cleanup

How to clean up function that is outside useEffect?


I have a resend code button and will disable it for 30sec after pressed. Here is my function.

    const time = useRef(30);

    function disableResendBtn() {
        time.current = 30;
        setsendCodeBtnDisabled(true);
        const interval = setInterval(() => {
            (time.current = time.current - 1),
                setsendCodeBtnText(i18n.t('RESEND') + '(' + time.current + ')'),
                console.log(time.current);
        }, 1000);
        setTimeout(() => {
            setsendCodeBtnDisabled(false);
            setsendCodeBtnText(i18n.t('RESEND'));
            clearInterval(interval);
        }, 30000);
    }

I am getting a error of Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a useEffect cleanup function when I move on to next page. This error is not showing everytime. So I am confused whether setInterval/setTimeout function is the reason.

Anyway, how I can use the useEffect to clean up that? The function is not inside useEffect.


Solution

  • You can store the setInterval's and setTimeout's return value in a ref and then create an empty useEffect which will then clean up those for you.

    There is a simpler way that I can think of. Instead of tracking these intervals and timeouts and then cleaning up using an empty useEffect, you can just create a useEffect which updates the current time every second. And, to track the disabled state, you can create another state which tracks "until when the button should be disabled".

    export default function App() {
      const [currentTime, setCurrentTime] = React.useState(Date.now());
      const [disabledUntil, setDisabledUntil] = React.useState(0);
    
      React.useEffect(() => {
        const interval = setInterval(() => {
          setCurrentTime(Date.now());
        }, 1000);
        return () => clearInterval(interval);
      }, []);
    
      const onClick = () => {
        // disable button for 30 seconds
        setDisabledUntil(Date.now() + 30 * 1000);
      };
    
      const buttonDisabledSeconds = Math.floor(
        (disabledUntil - currentTime) / 1000
      );
    
      return (
        <div>
          <button onClick={onClick} disabled={currentTime < disabledUntil}>
            Send
          </button>
          <p>
            {buttonDisabledSeconds >= 0 &&
              `Button disabled for ${buttonDisabledSeconds} seconds`}
          </p>
        </div>
      );
    }
    

    This way, the cleanup is very close to the actual interval definition.