za1234ki
za1234ki

Reputation: 155

React useState and useEffect nesting. Is this okay?

I'm playing with React hooks, and I'm trying to make a very basic clock. It's working fine and dandy, even the profiler says there are no weird rerenders going on. The part that worries me a bit is where useEffect calls updateTimeAndDate, which changes the state. I feel like it should go into a rerender loop. Why does it still work? I can't put it any better, sorry. :D

Could it also be a bit nicer? Here it is:

const addZero = (trunk) => (trunk >= 10 ? "" : "0") + trunk;

const [hours, setHours] = useState(addZero(0));
const [minutes, setMinutes] = useState(addZero(0));

let timeAndDate;

function updateTimeAndDate() {
  timeAndDate = new Date();

  setHours(addZero(timeAndDate.getHours()));
  setMinutes(addZero(timeAndDate.getMinutes()));
}

useEffect(() => {
  updateTimeAndDate();
});

setInterval(updateTimeAndDate, 500);

I put useEffect there to update the time immediately after loading the page, instead of waiting for a long long half second.

Upvotes: 0

Views: 751

Answers (2)

Rostyslav
Rostyslav

Reputation: 2866

To answer your questions

Is it ok?

Calling updateTimeAndDate (which updates the state) inside useEffect is ok. Nevertheless, in your current code you will face issues with interval as @RobertCooper mentioned.

However, I disagree with Robert on one thing. And it is that you will fall in the infinite loop. This leads us to your second question:

I feel like it should go into a rerender loop. Why does it still work?

Your current code does not fall into infinite loop due to the way React handles effects.

According to the documentation:

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects.

Therefore, as you update the state with same hour and same minute React does not fire your effect. Hence, there is no infinite loop in this particular case.

Upvotes: 0

Robert Cooper
Robert Cooper

Reputation: 2240

There are a couple issues that you will encounter with the code you've showed:

  • You will run an infinitely loop that causes your component to rapidly re-render. This is because you are calling setInterval on every render, and by calling updateTimeAndDate within setInterval, you are updating state, which in turns causes the component to re-render.

  • You have not specified a dependency array in your useEffect, so it will run on every re-render, further amplifying the infinite loop problem.

A possible alternative would be to only call the useEffect once by specifying an empty dependency array. You can also clear the interval when your component unmounts (by specifying a return value in your useEffect).

Here's a codesandbox demonstrating: https://codesandbox.io/s/stack-overflow-set-interval-89spq

import React, { useState, useEffect } from "react";
import "./styles.css";

export default function App() {
  const addZero = trunk => (trunk >= 10 ? "" : "0") + trunk;

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

  let timeAndDate;

  function updateTimeAndDate() {
    timeAndDate = new Date();

    setHours(addZero(timeAndDate.getHours()));
    setMinutes(addZero(timeAndDate.getMinutes()));
    setSeconds(addZero(timeAndDate.getSeconds()));
  }

  useEffect(() => {
    const interval = setInterval(updateTimeAndDate, 1000);
    return () => clearInterval(interval);
  }, []);

  // setInterval(updateTimeAndDate, 500);

  return (
    <div className="App">
      <h1>{hours}</h1>
      <h2>{minutes}</h2>
      <h3>{seconds}</h3>
    </div>
  );
}

Upvotes: 1

Related Questions