Understanding React hook useEffect & best practice

I have a personal greeting component that is setup like:

const PersonalGreeting = (): ReactElement => {
  const [timeOfDay, setTimeOfDay] = useState(null)
  const [date, setDate] = useState(new Date())

  useEffect(() => {
    const timer = setInterval(() => {
      setDate(new Date())
    }, 1000)

    setTimeOfDay(getTimeOfDay(date))

    return () => {
      clearInterval(timer)
    }
  }, [date])

  return (
    <>
    Good {timeOfDay}, Name
    </>
  )
}

When I console.log outside of the useEffect function, it seems to run twice. Inside the useEffect function it runs once, I was wondering if I was setting this up incorrectly or if there was a better way of doing this.

Helper function –

const getTimeOfDay = (date: Date): string => {
  let timeOfDay = ''
  const hour = date.getHours()
  const minute = date.getMinutes()
  const isOClock = minute === 0

  if ((hour >= 4 && hour < 12) || (hour === 12 && isOClock)) {
    timeOfDay = 'Morning'
  }

  if ((hour >= 12 && !isOClock && hour < 17) || (hour === 17 && isOClock)) {
    timeOfDay = 'Afternoon'
  }

  if ((hour >= 17 && !isOClock) || hour < 4) {
    timeOfDay = 'Evening'
  }

  return timeOfDay
}

21 thoughts on “Understanding React hook useEffect & best practice”

  1. You mentioned best practice so I’ll just cover what occurs to me. There’s actually a lot of cool things at play here.

    As others have mentioned useEffect runs after every render, including the first. It’s effectively both componentDidMount and componentDidUpdate.

    Render is running twice because the useEffect is changing react state after the first render.

    • First Render -> Sets up effect
    • Effect triggers immediately afterwards -> changes timeOfDay
    • React runs render again to deal with the new state of timeOfDay

    The simplest fix would be to set the first timeOfDay as a default during the first render. This way it won’t have to change during the effect.

      const [date, setDate] = useState(new Date())
      const [timeOfDay, setTimeOfDay] = useState(getTimeOfDay(date))
    

    Then move the code to set timeOfDay inside the interval with setDate.

    You can actually do better, though, because just by putting the time of day in react state you’re breaking the minimal essential state principle, which is fairly important in declarative programming. (note: I just made up that term. I don’t think there’s a good term for it yet.) What I mean by this is that timeOfDay is entirely derived from date, which is already in react state, so there is no reason (besides performance issues) to have both held in react state. Anytime this happens you should consider eliminating the redundant state. In this case this would involve deleting the timeOfDay state and just writing

        <>
        Good {getTimeOfDay(date)}, Name
        </>
    

    As far as I can tell, the only reason to ever hold onto timeOfDay as state is if your getTimeOfDay were expensive and if rendering happened more often than once a second, but even in that case you would just want to use reacts memoization. That would look like

    const timeOfDay = useMemo(() => getTimeOfDay(date), [date]);
    

    Looks similar, right? It’s basically the same, but it only runs during the render, and therefore can’t cause the double render issue you’re having. Specifically, useMemo will run getTimeOfDay during the first render, but then will only rerun getTimeOfDay on any subsequent render if [date] has changed; otherwise it will return the previous timeOfDay that was used.

    One last thing worth mentioning. You pass [date] into your useEffect, but this is actually wrong. It will work, but it’s not the intended use case. That array is used to tell react what state the effect function is reading from to cause its side-effects, but you’re not reading from date, so you don’t need to rerun the effect when date changes. What you really want is to pass in []—no state. That way react knows that you side-effect doesn’t depend on anything, and should only run once during the first render. Right now your use of an interval rather than a timeout is kind-of pointless, as react is canceling and reestablishing the interval on every render.

    With all the changes I’ve described so far your component would look like.

    const PersonalGreeting = (): ReactElement => {
      const [date, setDate] = useState(new Date())
    
      useEffect(() => {
        const timer = setInterval(() => {
          setDate(new Date())
        }, 1000)
    
        return () => {
          clearInterval(timer)
        }
      }, [])
    
      return (
        `Good ${getTimeOfDay(date)}, Name`
      )
    }
    

    I also switched to string templating, which I think is nicer, but that’s more of an opinion.

    Reply

Leave a Comment