Ar-51
Ar-51

Reputation: 115

how do I clearInterval on-click, with React Hooks?

I'm trying to refactor my code to react hooks, but I'm not sure if i'm doing it correctly. I tried copying and pasting my setInterval/setTimout code into hooks, but it did not work as intended. After trying different things I was able to get it to work, but I'm not sure if this is the best way to do it.

I know i can use useEffect to clear interval on un-mount, but I want to clear it before un-mounting.

Is the following good practice and if not what is a better way of clearing setInterval/setTimout before un-mounting?

Thanks,

useTimeout

import { useState, useEffect } from 'react';

let timer = null;

const useTimeout = () => {
    const [count, setCount] = useState(0);
    const [timerOn, setTimerOn] = useState(false);

    useEffect(() => {
        if (timerOn) {
            console.log("timerOn ", timerOn);

            timer = setInterval(() => {
                setCount((prev) => prev + 1)
            }, 1000);

        } else {
            console.log("timerOn ", timerOn);
            clearInterval(timer);
            setCount(0);
        }
    return () => {
        clearInterval(timer);
    }
    }, [timerOn])

    return [count, setCount, setTimerOn];
}

export default useTimeout;

Component

import React from 'react';
import useTimeout from './useTimeout';

const UseStateExample = () => {
    const [count, setCount, setTimerOn] = useTimeout()
    return (
        <div>
            <h2>Notes:</h2>
            <p>New function are created on each render</p>
            <br />
            <h2>count = {count}</h2>
            <button onClick={() => setCount(prev => prev + 1)}>Increment</button>
            <br />
            <button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
            <br />
            <button onClick={() => setTimerOn(true)}>Set Interval</button>
            <br />
            <button onClick={() => setTimerOn(false)}>Stop Interval</button>
            <br />

        </div>
    );
}

export default UseStateExample;

Upvotes: 8

Views: 21706

Answers (4)

After many attempts to make a timer work with setInterval, I decided to use setTimeOut, I hope it works for you.

  const [count, setCount] = useState(60);

  useEffect(() => {
    if (count > 0) {
      setTimeout(() => {
        setCount(count - 1);
      }, 1000);
    }
  }, [count]);

Upvotes: 0

陈桥驿站
陈桥驿站

Reputation: 1

Demo of clear many timers.

You should declare and clear timer.current instead of timer.

  1. Declare s and timer.
const [s, setS] = useState(0);
let timer = useRef<NodeJS.Timer>();
  1. Initialize timer in useEffect(() => {}).
useEffect(() => {
  if (s == props.time) {
    clearInterval(timer.current);
  }
  return () => {};
}, [s]);
  1. Clear timer.
useEffect(() => {
  if (s == props.time) {
    clearInterval(timer.current);
  }
  return () => {};
}, [s]);

Upvotes: 0

Matt Wills
Matt Wills

Reputation: 726

In this example, we add a couple of things...

  1. A on/off switch for the timeout (the 'running' arg) which will completely switch it on or off

  2. A reset function, allowing us to set the timeout back to 0 at any time:

    If called while it's running, it'll keep running but return to 0. If called while it's not running, it'll start it.

const useTimeout = (callback, delay, running = true) => {
  // save id in a ref so we make sure we're always clearing the latest timeout
  const timeoutId = useRef('');

  // save callback as a ref so we can update the timeout callback without resetting it
  const savedCallback = useRef();
  useEffect(
    () => {
      savedCallback.current = callback;
    },
    [callback],
  );

  // clear the timeout and start a new one, updating the timeoutId ref
  const reset = useCallback(
    () => {
      clearTimeout(timeoutId.current);

      const id = setTimeout(savedCallback.current, delay);
      timeoutId.current = id;
    },
    [delay],
  );

  // keep the timeout dynamic by resetting it whenever its' deps change
  useEffect(
    () => {
      if (running && delay !== null) {
        reset();

        return () => clearTimeout(timeoutId.current);
      }
    },
    [delay, running, reset],
  );

  return { reset };
};

So in your example above, we could use it like so...

const UseStateExample = ({delay}) => {
    // count logic
    const initCount = 0
    const [count, setCount] = useState(initCount)
    
    const incrementCount = () => setCount(prev => prev + 1)
    const decrementCount = () => setCount(prev => prev - 1)
    const resetCount = () => setCount(initCount)

    // timer logic
    const [timerOn, setTimerOn] = useState(false)
    const {reset} = useTimeout(incrementCount, delay, timerOn)

    const startTimer = () => setTimerOn(true)
    const stopTimer = () => setTimerOn(false)
    
    return (
        <div>
            <h2>Notes:</h2>
            <p>New function are created on each render</p>
            <br />
            <h2>count = {count}</h2>
            <button onClick={incrementCount}>Increment</button>
            <br />
            <button onClick={decrementCount}>Decrement</button>
            <br />
            <button onClick={startTimer}>Set Interval</button>
            <br />
            <button onClick={stopTimer}>Stop Interval</button>
            <br />
            <button onClick={reset}>Start Interval Again</button>
            <br />

        </div>
    );
}

Upvotes: 3

Yuanqiu Li
Yuanqiu Li

Reputation: 1840

--- added @ 2019-02-11 15:58 ---

A good pattern to use setInterval with Hooks API:

https://overreacted.io/making-setinterval-declarative-with-react-hooks/


--- origin answer ---

Some issues:

  1. Do not use non-constant variables in the global scope of any modules. If you use two instances of this module in one page, they’ll share those global variables.

  2. There’s no need to clear timer in the “else” branch because if the timerOn change from true to false, the return function will be executed.

A better way in my thoughts:

import { useState, useEffect } from 'react';

export default (handler, interval) => {
  const [intervalId, setIntervalId] = useState();
  useEffect(() => {
    const id = setInterval(handler, interval);
    setIntervalId(id);
    return () => clearInterval(id);
  }, []);
  return () => clearInterval(intervalId);
};

Running example here:

https://codesandbox.io/embed/52o442wq8l?codemirror=1

Upvotes: 18

Related Questions