1

How to use an onClick event to stop a recurring function call? The function call is recurring due to using useEffect, setInterval and clearInterval.

An example shown in this article, the below code will run forever. How to stop the function from being called once the <header></header> is clicked?

import React, { useState, useEffect } from 'react';

const IntervalExample = () => {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(seconds => seconds + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        {seconds} seconds have elapsed since mounting.
      </header>
    </div>
  );
};

export default IntervalExample;
Greg
  • 8,175
  • 16
  • 72
  • 125
  • You basiclly can't because you declared it a const with nothing that can change his value inside the useEffect, so nothing will ever be able to reach it . But you can declare it outside the use effect and change the value of `interval` with a useEffect on `seconds` or something like . – TheoWckr Apr 17 '20 at 14:41

3 Answers3

2

Add another state called running, and make the useEffect dependent on it. Whenever running is toggled to false, the interval would be cleared by the cleanup function of useEffect. Since running is false we skip setting a new interval. Whenever it's toggled to true, it will be restarted.

const { useState, useEffect, useCallback } = React;

const IntervalExample = () => {
  const [seconds, setSeconds] = useState(0);
  const [running, setRunning] = useState(true);
  
  const toggleRunning = useCallback(
    () => setRunning(run => !run)
  , []);

  useEffect(() => {
    if(!running) {
      // setSeconds(0); // if you want to reset it as well
      return;
    }
    
    const interval = setInterval(() => {
      setSeconds(seconds => seconds + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, [running]);

  return (
    <div className="App">
      <header className="App-header">
        {seconds} seconds have elapsed since mounting.
      </header>
      
      <button
        onClick={toggleRunning}
        >
        {running ? 'running' : 'stopped'}
      </button>
    </div>
  );
};

ReactDOM.render(
  <IntervalExample />,
  root
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • Have a question on this approach, when we update `running` hook, it won't unmount the component so the cleanup won't work. And you will generate a new interval id at this time and the old interval won't be cleared. Am I right? – Yash Joshi Apr 17 '20 at 14:48
  • 1
    The cleanup is called whenever the [dependencies change](https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect): "Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect.". If not the setInterval would continue to call `setSeconds`. – Ori Drori Apr 17 '20 at 14:50
1

I would pull out the interval const from the useEffect function like this.

import React, { useState, useEffect } from 'react';

const IntervalExample = () => {
  const [seconds, setSeconds] = useState(0);
  let interval;

  useEffect(() => {
    interval = setInterval(() => {
      setSeconds(seconds => seconds + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);


  const handleClick = () => {
    if (interval) {
      clearInterval(interval);
    }
  }

  return (
    <div className="App">
      <header className="App-header" onClick={() => handleClick()}>
        {seconds} seconds have elapsed since mounting.
      </header>
    </div>
  );
};

export default IntervalExample;
raven
  • 2,381
  • 2
  • 20
  • 44
  • `interval` seems to be undefined inside the `handleClick` function and the above code doesn't seem to work – Greg Apr 17 '20 at 15:02
1

I think by stopping you mean to say clearInterval on a click event. So the best way to hold an interval id in a new state variable.


const IntervalExample = () => {
  const [seconds, setSeconds] = useState(0);
  const [currentIntervalId, setIntervalId] = useState(null);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(seconds => seconds + 1);
    }, 1000);
    setIntervalId(interval);
    return () => clearInterval(interval);
  }, []);

  const stopInterval = () => {
    clearInterval(currentIntervalId);
  };

  return (
    <div className="App">
      <header className="App-header" onClick={stopInterval}>
        {seconds} seconds have elapsed since mounting.
      </header>
    </div>
  );
};
  • Why in state? Because this will give you more flexibility to restart the interval again or to stop whenever you want based on event.

Note: You can also put this in a variable defined outside the scope of useEffect.

Yash Joshi
  • 2,586
  • 1
  • 9
  • 18