1

I'm practicing react hooks and I'm creating a very simple stopwatch app. Currently, my code is doing exactly what I want it to do but I do not understand why it works. When I hit start, the setTimeouts run and constantly update the time state. When I hit stop, it clears the timeout. Why does it clear the timeout when I do not explicitly tell it to. Also, based on the react docs, the return in useEffect will only run when the component unmounts. However, I threw console.logs inside and saw that it runs the returned callback every time useEffect is called. Finally, I removed the returned callback and saw that it doesn't actually clear the timeout when I hit stop. Can someone help me dissect this?

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

function Stopwatch(){

  const [time, setTime] = useState(0);
  const [start, setStart] = useState(false);

  useEffect(() => {
    let timeout;
    if (start) {
      timeout = setTimeout(() => {setTime(currTime => currTime + 1);}, 1000);
    }

    return () => {
      clearTimeout(timeout);
    }
  });

  return(
    <>
      <div>{time}</div>
      <button onClick={() => setStart(currStart => !currStart)}>{start ? "Stop" : "Start"}</button>
    </>
  )
}

export default Stopwatch
Timothy Jao
  • 13
  • 1
  • 3

1 Answers1

8

Why does it clear the timeout when I do not explicitly tell it to?

In your implementation useEffect runs after every re-render because you didn't specify the dependencies array, so if you start the timer and then in the middle press stop the clean up function is going to run and the last timeout will be cleared

It goes like this,

The component mounts -> useEffect callback fires and returns a function -> when the component re-renders, the returned function is executed and the cycle goes back to running the useEffect callback.

What you probably read in the docs had an empty dependencies array which is the second argument of useEffect

useEffect(() => {
  console.log('will only run when the component mounts for the first time')

  return () => {
    console.log('will only run when the component unmounts')
  }
}, []) // nothing inside the dependencies array, run this once

A better implementation of your component will be like this

function Stopwatch(){
  const [time, setTime] = useState(0)
  const [start, setStart] = useState(false)

  useEffect(() => {
    // when start is false there is no reason to set up a timer or return a
    // cleanup function so lets just exit early
    if (!start) return 

    // start is true, set up the interval
    const intervalId = setInterval(() => setTime(prevTime => prevTime + 1), 1000)
    // return a cleanup function that will run only when start changes
    // to false
    return () => clearInterval(intervalId)
  }, [start]) // run this effect only when start changes

  const toggleStart = () => setStart(prevStart => !prevStart)

  return(
    <>
      <div>{time}</div>
      <button onClick={toggleStart}>{start ? "Stop" : "Start"}</button>
    </>
  )
}
Asaf Aviv
  • 11,279
  • 1
  • 28
  • 45
  • Thank you for your explanation! I just have a question if you have the time: When does the returned function in useEffect get run? Based on the docs, it sounds like the clearTimeout only runs when it the component unmounts. However, if I throw a console.log into that returned callback, it will console.log into the developer tools. However, the interval is clearly not cleared as the timer is running. What exactly does that call back do? Also, in the code you put above, why does the setInterval get cleared when you press stop? It doesn't seem like you called clearInterval anywhere. – Timothy Jao Dec 18 '19 at 17:44
  • @TimothyJao In your implementation you didn't specify the second argument of `useEffect` which is the dependencies array so the returned callback will run on every re-render. In my implementation(which had a typo, i meant to call `clearInterval`) because i put `[start]` as the second argument to `useEffect` the returned callback is going to run only when start changes from true to false – Asaf Aviv Dec 18 '19 at 17:52
  • Just to clarify, does the returned callback in useEffect always run? Based on the react-hooks docs, I thought the returned callback only runs once the component dismounts. – Timothy Jao Dec 18 '19 at 20:53
  • 1
    @TimothyJao It runs any time one of the variables inside the dependencies array changes, if the array is empty it will run only on unmount, if there is no array at all it will run on every re-render – Asaf Aviv Dec 18 '19 at 20:56