0

I'm working on building a clock that counts up, just a practice exercise (I know there are better ways of doing this).

My issue is when a minute is added, the "addMinute" seems to run twice, and I can't for the life of me figure out how or why.

Here is a demo on codesandbox: https://codesandbox.io/s/restless-frost-bud7p And here is the code at a glance:

(please note, the counter only counts up to 3 and counts faster; this is just for testing purposes)

const Clock = (props) => {

    const [seconds, setSeconds] = useState(0)
    const [minutes, setMinutes] = useState(0)
    const [hours, setHours] = useState(0)

    const addHour = () => {
        setHours(p => p + 1)
    }
    const addMinute = () => {
        setMinutes(prev => {
            if (prev === 3) {
                addHour()
                return 0
            } else {
                return prev + 1
            }
        })
    }
    const addSecond = () => {
        setSeconds(prevState => {
            if (prevState === 3) {
                addMinute()
                return 0
            } else {
                return prevState + 1
            }
        })
    }
    
    

    useEffect(() => {
        const timer = setInterval(addSecond, 600)
        return () => clearInterval(timer)
    }, [])

    return (
        <div>
            <h1>time!</h1>
            <p>
                {hours < 10 ? 0 : ""}{hours}
                :
                {minutes < 10 ? 0 : ""}{minutes}
                :
                {seconds < 10 ? 0 : ""}{seconds}
            </p>
            <p>
                {seconds.toString()}
            </p>
        </div>
    )
}
m4cbeth
  • 144
  • 1
  • 1
  • 11
  • 1
    I can't reproduce the issue. How do you know that `addMinute()` is being called twice? – Turtlean Dec 27 '20 at 22:47
  • 1
    Please read the first answer to the question linked in this comment (I suspect this question is a duplicate): https://stackoverflow.com/questions/61254372/my-react-component-is-rendering-twice-because-of-strict-mode -- rather than removing the protection that StrictMode provides, take advantage of it while knowing the consequences. – Randy Casburn Dec 27 '20 at 23:01
  • @Turtlean After adding some console logs it appears it is not running twice. However, when the addMinute runs, somehow when I render the state, it's showing as "2" before flipping back to zero. – m4cbeth Dec 28 '20 at 03:19

3 Answers3

1

The issue is that you are using the React.StrictMode wrapper in the index.js file.

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects

So you should decide between using strict mode or having side effects, the easy way is just removing the React.StrictMode wrapper. The other way is removing side effects, where you only need to do the following:

Update your addSecond and addMinute functions to something like:

const addMinute = () => {
  setMinutes((prev) => prev + 1);
};

const addSecond = () => {
  setSeconds((prevState) => prevState + 1);
};

And your useEffect call to something like:

useEffect(() => {
  if(seconds === 3) {
    addMinute();
    setSeconds(0);
  };

  if(minutes === 3) {
    addHour();
    setMinutes(0);
  } 

  const timer = setInterval(addSecond, 600);
  return () => clearInterval(timer);
}, [seconds, minutes]);

Here an updated version of your code: https://codesandbox.io/s/goofy-lake-1i9xf

fadfa
  • 76
  • 1
  • 4
0

A couple of issues,

first you need to use prev for minutes, so

const addMinute = () => {
    setMinutes(prev => {
        if (prev === 3) {
            addHour()
            return 0
        } else {
            return prev + 1
        }
    })
}

And then you need to remove the React.StrictMode wrapper component from index, which is what is actually causing the double increase, as part of what the strict mode does is

This is done by intentionally double-invoking the following functions:

Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer

see: https://codesandbox.io/s/pensive-wildflower-pujmk

Gabriele Petrioli
  • 191,379
  • 34
  • 261
  • 317
  • Why would the OP remove a defensive programming step such as `React.StrictMode`? That makes little sense. – Randy Casburn Dec 27 '20 at 22:59
  • Because it causes double re-renders and is causing the problem they are experiencing. – Gabriele Petrioli Dec 28 '20 at 00:02
  • Yes that's right. I only had it like that because I had tried that out of desperation. I've made the adjustment. However, this is not the cause of the issue, as it still somehow is called twice – m4cbeth Dec 28 '20 at 03:06
  • @m4cbeth did you notice the part about `React.StrictMode` in the index file ? That is what is causing the problem. – Gabriele Petrioli Dec 28 '20 at 08:24
  • @Gabriele - Yes, I do understand that, if you notice my comment to the OP's question. My point is that you should have described _why_ it should be removed in your answer. It would have been a better explanation. This is just my opinion. – Randy Casburn Dec 28 '20 at 14:45
0

So I had no idea about strict mode and the intentional double renders. After reading the documentation I finally understand the purpose of this.

As such, it appears the best solution is to have no side effects from the useEffect, and instead, handle that logic outside of the effect, but still changing every second.

So, I set an effect that has a piece of state starting at zero and going up by one per second.

Then, with each change of "time", the useMemo will recalculate how many hours, mins and seconds the total time is.

The only thing I don't like is all those calculations running every render! (But realistically those take but a few miliseconds, so performance doesn't seem to be an issue).

    const [time, setTime] = useState(0)

    useEffect(() => {
        const timer = setInterval(() => {
            setTime(p => p + 1)
        }, 999);
        return () => clearTimeout(timer);
    }, [userState]);

    const timeJSON = useMemo(() => {
        const hrs = Math.floor(time/3600)
        const mins = Math.floor( (time-(3600*hrs)) / 60 )
        const secs = time - (3600 * hrs) - (60*mins)

        return {
            hrs,
            mins,
            secs,
        }

    }, [time])

    return (
        <div>            
            <p>             
                {timeJSON.hrs < 10 ? 0 : ""}{timeJSON.hrs}
                :
                {timeJSON.mins < 10 ? 0 : ""}{timeJSON.mins}
                :
                {timeJSON.secs < 10 ? 0 : ""}{timeJSON.secs}
                
            </p>
        </div>
    )

Thanks again for everyone pointing me in the right direction on this!

m4cbeth
  • 144
  • 1
  • 1
  • 11